diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..b83632eaa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.htaccess
+node_modules
+package-lock.json
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..f3d0d2159
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+ - "node"
diff --git a/Bangle.js.svg b/Bangle.js.svg
new file mode 100644
index 000000000..90c908c9b
--- /dev/null
+++ b/Bangle.js.svg
@@ -0,0 +1,478 @@
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..4cfef69ac
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+App Loader ChangeLog
+====================
+
+Changed for individual apps are listed in `apps/appname/ChangeLog`
+
+* `Remove All Apps` now doesn't perform a reset before erase - fixes inability to update firmware if settings are wrong
+* Added optional `README.md` file for apps
diff --git a/README.md b/README.md
index 0f453e0bb..3f6c82c02 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,14 @@
Bangle.js App Loader (and Apps)
================================
-Try it live at [banglejs.com/apps](https://banglejs.com/apps)
+[](https://travis-ci.org/espruino/BangleApps)
+
+* Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps)
+* Try the **development version** at [github.io](https://espruino.github.io/BangleApps/)
+
+**All software (including apps) in this repository is MIT Licensed - see [LICENSE](LICENSE)** By
+submitting code to this repository you confirm that you are happy with it being MIT licensed,
+and that it is not licensed in another way that would make this impossible.
## How does it work?
@@ -12,15 +19,26 @@ it with the files it sees in the watch's storage.
* To upload an app, BangleAppLoader checks the files that are
listed in `apps.json`, loads them, and sends them over Web Bluetooth.
+## Getting Started
+
+Check out:
+
+* [Building your first Bangle.js Application](https://www.espruino.com/Bangle.js+First+App)
+* [Adding an app to the Bangle.js App Loader](https://www.espruino.com/Bangle.js+App+Loader)
+* [Customising the App Loader](https://www.espruino.com/Bangle.js+App+Loader+Custom)
+
## What filenames are used
Filenames in storage are limited to 8 characters. To
easily distinguish between file types, we use the following:
-* `+stuff` is JSON for an app
-* `*stuff` is an image
-* `-stuff` is JS code
-* `=stuff` is JS code for stuff that is run at boot time - eg. handling settings or creating widgets on the clock screen
+* `stuff.info` is JSON that describes an app - this is auto-generated by the App Loader
+* `stuff.img` is an image
+* `stuff.app.js` is JS code for applications
+* `stuff.wid.js` is JS code for widgets
+* `stuff.settings.js` is JS code for the settings menu
+* `stuff.boot.js` is JS code that automatically gets run at boot time
+* `stuff.json` is used for JSON settings for an app
## Developing your own app
@@ -31,35 +49,25 @@ easily distinguish between file types, we use the following:
## Adding your app to the menu
-* Come up with a unique 7 character name, we'll assume `7chname`
+* Come up with a unique (all lowercase, nu spaces) name, we'll assume `7chname`. Bangle.js
+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/7chname`
* We'd recommend that you copy files from 'Example Applications' (below) as a base, or...
* `apps/7chname/app.png` should be a 48px icon
* Use http://www.espruino.com/Image+Converter to create `apps/7chname/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String"
-* Create an entry in `apps/7chname/app.json` as follows:
-
-```
-{
- "name":"Short Name",
- "icon":"*7chname",
- "src":"-7chname"
-}
-```
-
-See `app.json / widget.json` below for more info on the correct format.
-
* Create an entry in `apps.json` as follows:
```
{ "id": "7chname",
"name": "My app's human readable name",
+ "shortName" : "Short Name",
"icon": "app.png",
"description": "A detailed description of my great app",
"tags": "",
"storage": [
- {"name":"+7chname","url":"app.json"},
- {"name":"-7chname","url":"app.js"},
- {"name":"*7chname","url":"app-icon.js","evaluate":true}
+ {"name":"7chname.app.js","url":"app.js"},
+ {"name":"7chname.img","url":"app-icon.js","evaluate":true}
],
},
```
@@ -76,36 +84,34 @@ This is the best way to test...
* Run your personal `Bangle App Loader` at https://\.github.io/BangleApps/index.html to load apps onto your device
* Your apps should be inside it - if there are problems, check your web browser's 'developer console' for errors
+**Note:** It's a great idea to get a local copy of the repository on your PC,
+then run `bin/sanitycheck.js` - it'll run through a bunch of common issues
+that there might be.
+
Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect.
### Offline
-You can add the following to the Espruino Web IDE:
+Using the 'Storage' icon in [the Web IDE](https://www.espruino.com/ide/)
+(4 discs), upload your files into the places described in your JSON:
-```
-// replace with your 7chname app name
-var appname = "mygreat";
+* `app-icon.js` -> `7chname.img`
-require("Storage").write('*'+appname,
- // place app-icon.js contents here
-);
+Now load `app.js` up in the editor, and click the down-arrow to the bottom
+right of the `Send to Espruino` icon. Click `Storage` and then either choose
+`7chname.app.js` (if you'd uploaded your app previously), or `New File`
+and then enter `7chname.app.js` as the name.
-//
-require("Storage").write("+"+appname,{
- "name":"My Great App","type":"",
- "icon":"*"+appname,
- "src":"-"+appname,
-});
+Now, clicking the `Send to Espruino` icon will load the app directly into
+Espruino **and** will automatically run it.
-require("Storage").write("-"+appname,`
-// place contents of app.js here
-// be aware of double-quoting templated strings
-`
-```
-
-When you upload code this way, your app will be uploaded to Bangle.js's menu
+When you upload code this way, your app will even be uploaded to Bangle.js's menu
without you having to use the `Bangle App Loader`
+**Note:** Widgets need to be run inside a clock or app, so if you're
+developing a widget you need to go go `Settings` -> `Communications` -> `Load after saving`
+and set it to `Load default application`.
+
## Example Applications
To make the process easier we've come up with some example applications that you can use as a base
@@ -113,21 +119,23 @@ when creating your own. Just come up with a unique 7 character name, copy `apps/
or `apps/_example_widget` to `apps/7chname`, and add `apps/_example_X/add_to_apps.json` to
`apps.json`.
+**If you're making a widget** please start the name with `wid` to make
+it easy to find!
+
### App Example
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 widget to bootloader and loader
+* `add_to_apps.json` - insert into `apps.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.json` - short app name for Bangle.js menu and storage filenames
* `app.js` - app code
#### `app-icon.js`
-The icon image and short description is used in the menu entry as selection posibility.
+The icon image and short description is used in Bangle.js's launcher.
Use the Espruino [image converter](https://www.espruino.com/Image+Converter) and upload your `app.png` file.
@@ -143,24 +151,43 @@ Follow this steps to create a readable icon as image string.
Replace this line with the image converter output:
```
-require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="));
+require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))
```
-Keep in mind to use this converter for creating images you like to draw with `g.drawImage()` with your app.
+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
+them, and then `Bangle.drawWidgets()` to draw them onto the screen whenever the app
+has call to completely clear the screen. Widgets themselves will update as and when needed.
### Widget Example
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
-* `widget.json` - short widget name and storage names
* `widget.js` - widget code
-### `app.json` / `widget.json` format
+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:
-This is the file that's loaded onto Bangle.js, which gives information
-about the app.
+```
+WIDGETS["mywidget"]={
+ area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
+ 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
+};
+```
+
+When the widget is to be drawn, `x` and `y` values are set up in `WIDGETS["mywidget"]`
+and `draw` can then use `this.x` and `this.y` to figure out where it needs to draw to.
+
+
+### `app.info` format
+
+This is the file that's **auto-generated** and loaded onto Bangle.js by the App Loader,
+and which gives information about the app for the Launcher.
```
{
@@ -183,20 +210,31 @@ about the app.
```
{ "id": "appid", // 7 character app id
"name": "Readable name", // readable name
+ "shortName": "Short name", // short name for launcher
"icon": "icon.png", // icon in apps/
"description": "...", // long description
- "type":"...", // optional(if app) - 'app'/'widget'/'launch'
+ "type":"...", // optional(if app) - 'app'/'widget'/'launch'/'bootloader'
"tags": "", // comma separated tag list for searching
+ "readme": "README.md", // if supplied, a link to a markdown-style text file
+ // that contains more information about this app (usage, etc)
+ // A 'Read more...' link will be added under the app
+
"custom": "custom.html", // if supplied, apps/custom.html is loaded in an
// iframe, and it must post back an 'app' structure
// like this one with 'storage','name' and 'id' set up
+ // see below for more info
+
+ "interface": "interface.html", // if supplied, apps/interface.html is loaded in an
+ // iframe, and it may interact with the connected Bangle
+ // to retrieve information from it
+ // see below for more info
"allow_emulator":true, // if 'app.js' will run in the emulator, set to true to
// add an icon to allow your app to be tested
"storage": [ // list of files to add to storage
- {"name":"-appid", // filename to use in storage
+ {"name":"appid.js", // filename to use in storage
"url":"", // URL of file to load (currently relative to apps/)
"content":"..." // if supplied, this content is loaded directly
"evaluate":true // if supplied, data isn't quoted into a String before upload
@@ -213,6 +251,120 @@ about the app.
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty.
* storage is used to identify the app files and how to handle them
+### `apps.json`: `custom` element
+
+Apps that can be customised need to define a `custom` element in `apps.json`,
+which names an HTML file in that app's folder.
+
+When `custom` is defined, the 'upload' button is replaced by a customize
+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`:
+
+```
+
+
+
+
+
+
+
+
+
+
+```
+
+This'll then be loaded in to the watch. See [apps/qrcode/grcode.html](the QR Code app)
+for a clean example.
+
+### `apps.json`: `interface` element
+
+Apps that create data that can be read back can define a `interface` element in `apps.json`,
+which names an HTML file in that app's folder.
+
+When `interface` is defined, a `Download from App` button is added to
+the app's description, and when clicked it opens the HTML page specified
+in an iframe.
+
+```
+
+
+
+
+
+
+
Loading...
+
+
+
+```
+
+When the page is ready a function called `onInit` is called,
+and in that you can call `Puck.write` and `Puck.eval` to get
+the data you require from Bangle.js.
+
+See [apps/gpsrec/interface.html](the GPS Recorder) for a full example.
+
+### Adding configuration to the "Settings" menu
+
+Apps (or widgets) can add their own settings to the "Settings" menu under "App/widget settings".
+To do so, the app needs to include a `settings.js` file, containing a single function
+that handles configuring the app.
+When the app settings are opened, this function is called with one
+argument, `back`: a callback to return to the settings menu.
+
+Example `settings.js`
+```js
+// make sure to enclose the function in parentheses
+(function(back) {
+ let settings = require('Storage').readJSON('app.settings.json',1)||{};
+ function save(key, value) {
+ settings[key] = value;
+ require('Storage').write('app.settings.json',settings);
+ }
+ const appMenu = {
+ '': {'title': 'App Settings'},
+ '< Back': back,
+ 'Monkeys': {
+ value: settings.monkeys||12,
+ onchange: (m) => {save('monkeys', m)}
+ }
+ };
+ E.showMenu(appMenu)
+})
+```
+In this example the app needs to add both `app.settings.js` and
+`app.settings.json` to `apps.json`:
+```json
+ { "id": "app",
+ ...
+ "storage": [
+ ...
+ {"name":"app.settings.js","url":"settings.js"},
+ {"name":"app.settings.json","content":"{}"}
+ ]
+ },
+```
+That way removing the app also cleans up `app.settings.json`.
+
## Coding hints
- use `g.setFont(.., size)` to multiply the font size, eg ("6x8",3) : "18x24"
@@ -227,7 +379,17 @@ about the app.
- using `g.setLCDBrightness()` can save you power during long periods with lcd on
-- chaining graphics methodes, eg `g.setColor(0xFD20).setFontAlign(0,0).setfont("6x8",3)`
+- chaining graphics methods, eg `g.setColor(0xFD20).setFontAlign(0,0).setfont("6x8",3)`
+
+### Misc Notes
+
+- Need to save state? Use the `E.on('kill',...)` event to save JSON to a file called `7chname.json`, then load it at startup.
+
+- 'Alarm' apps define a file called `alarm.js` which handles the actual alarm window.
+
+- Locale is handled by `require("locale")`. An app may create a `locale` file in Storage which is
+a module that overwrites Bangle.js's default locale.
+
### Graphic areas
@@ -286,7 +448,7 @@ You can use `g.setColor(r,g,b)` OR `g.setColor(16bitnumber)` - some common 16 bi
The [`testing`](testing) folder contains snippets of code that might be useful for your apps.
* `testing/colors.js` - 16 bit colors as name value pairs
-* `testing/gpstrack.js` - code to store a GPS track in Bagle.js storage and output it back to the console
+* `testing/gpstrack.js` - code to store a GPS track in Bangle.js storage and output it back to the console
* `testing/map` - code for splitting an image into map tiles and then displaying them
## Credits
diff --git a/appinfo.js b/appinfo.js
deleted file mode 100644
index 7d49647fe..000000000
--- a/appinfo.js
+++ /dev/null
@@ -1,55 +0,0 @@
-function toJS(txt) {
- return JSON.stringify(txt);
-}
-
-var AppInfo = {
- getFiles : (app,fileGetter) => {
- return new Promise((resolve,reject) => {
- // Load all files
- Promise.all(app.storage.map(storageFile => {
- if (storageFile.content)
- return Promise.resolve(storageFile);
- else if (storageFile.url)
- return fileGetter(`apps/${app.id}/${storageFile.url}`).then(content => {
- return {
- name : storageFile.name,
- content : content,
- evaluate : storageFile.evaluate
- }});
- else return Promise.resolve();
- })).then(fileContents => { // now we just have a list of files + contents...
- // filter out empty files
- fileContents = fileContents.filter(x=>x!==undefined);
- // then map each file to a command to load into storage
- fileContents.forEach(storageFile => {
- // check if this is the JSON file
- if (storageFile.name[0]=="+") {
- storageFile.evaluate = true;
- var json = {};
- try {
- json = JSON.parse(storageFile.content);
- } catch (e) {
- reject(storageFile.name+" is not valid JSON");
- }
- if (app.version) json.version = app.version;
- json.files = fileContents.map(storageFile=>storageFile.name).join(",");
- storageFile.content = JSON.stringify(json);
- }
- // format ready for Espruino
- var js;
- if (storageFile.evaluate) {
- js = storageFile.content.trim();
- if (js.endsWith(";"))
- js = js.slice(0,-1);
- } else
- js = toJS(storageFile.content);
- storageFile.cmd = `\x10require('Storage').write(${toJS(storageFile.name)},${js});`;
- });
- resolve(fileContents);
- }).catch(err => reject(err));
- });
- },
-};
-
-if ("undefined"!=typeof module)
- module.exports = AppInfo;
diff --git a/apps.json b/apps.json
index c9c59d34b..d8cdde0fc 100644
--- a/apps.json
+++ b/apps.json
@@ -1,756 +1,1435 @@
[
- { "id": "boot",
+ {
+ "id": "boot",
"name": "Bootloader",
"icon": "bootloader.png",
- "version":"0.03",
+ "version": "0.14",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"tags": "tool,system",
+ "type": "bootloader",
"storage": [
- {"name":".boot0","url":"boot0.js"},
- {"name":".bootcde","url":"bootloader.js"},
- {"name":"+boot","url":"bootloader.json"}
+ {
+ "name": ".boot0",
+ "url": "boot0.js"
+ },
+ {
+ "name": ".bootcde",
+ "url": "bootloader.js"
+ }
],
- "sortorder" : -10
+ "sortorder": -10
},
- { "id": "launch",
- "name": "Launcher",
+ {
+ "id": "moonphase",
+ "name": "Moonphase",
"icon": "app.png",
- "version":"0.01",
+ "version": "0.02",
+ "description": "Shows current moon phase. Now with GPS function.",
+ "tags": "",
+ "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",
+ "icon": "app.png",
+ "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.",
+ "tags": "",
+ "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": "launch",
+ "name": "Default Launcher",
+ "shortName": "Launcher",
+ "icon": "app.png",
+ "version": "0.01",
"description": "This is needed by Bangle.js to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
"tags": "tool,system,launcher",
+ "type": "launch",
"storage": [
- {"name":"+launch","url":"app.json"},
- {"name":"-launch","url":"app.js"}
+ {
+ "name": "launch.app.js",
+ "url": "app.js"
+ }
],
- "sortorder" : -10
+ "sortorder": -10
},
- { "id": "gbridge",
+ {
+ "id": "about",
+ "name": "About",
+ "icon": "app.png",
+ "version": "0.04",
+ "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
+ "tags": "tool,system",
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "about.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "about.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "locale",
+ "name": "Languages",
+ "icon": "locale.png",
+ "version": "0.05",
+ "description": "Translations for different countries",
+ "tags": "tool,system,locale,translate",
+ "type": "locale",
+ "custom": "locale.html",
+ "storage": [
+ {
+ "name": "locale"
+ }
+ ],
+ "sortorder": -10
+ },
+ {
+ "id": "welcome",
+ "name": "Welcome",
+ "icon": "app.png",
+ "version": "0.06",
+ "description": "Appears at first boot and explains how to use Bangle.js",
+ "tags": "start,welcome",
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "welcome.boot.js",
+ "url": "boot.js"
+ },
+ {
+ "name": "welcome.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "welcome.settings.js",
+ "url": "settings.js"
+ },
+ {
+ "name": "welcome.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "gbridge",
"name": "Gadgetbridge",
"icon": "app.png",
- "version":"0.01",
+ "version": "0.07",
"description": "The default notification handler for Gadgetbridge notifications from Android",
- "tags": "tool,system,android",
+ "tags": "tool,system,android,widget",
+ "type": "widget",
"storage": [
- {"name":"+gbridge","url":"app.json"},
- {"name":"-gbridge","url":"app.js"},
- {"name":"*gbridge","url":"app-icon.js","evaluate":true},
- {"name":"=gbridge","url":"widget.js"}
+ {
+ "name": "gbridge.settings.js",
+ "url": "settings.js"
+ },
+ {
+ "name": "gbridge.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ },
+ {
+ "name": "gbridge.wid.js",
+ "url": "widget.js"
+ }
]
},
- { "id": "mclock",
+ {
+ "id": "mclock",
"name": "Morphing Clock",
"icon": "clock-morphing.png",
- "version":"0.02",
+ "version": "0.03",
"description": "7 segment clock that morphs between minutes and hours",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+mclock","url":"clock-morphing.json"},
- {"name":"-mclock","url":"clock-morphing.js"},
- {"name":"*mclock","url":"clock-morphing-icon.js","evaluate":true}
+ {
+ "name": "mclock.app.js",
+ "url": "clock-morphing.js"
+ },
+ {
+ "name": "mclock.img",
+ "url": "clock-morphing-icon.js",
+ "evaluate": true
+ }
],
- "sortorder" : -9
+ "sortorder": -9
},
- { "id": "setting",
+ {
+ "id": "setting",
"name": "Settings",
"icon": "settings.png",
- "version":"0.02",
- "description": "A menu for setting up Bangle.js - by default this disables Bluetooth unless you enable 'BLE' AND 'Dev'",
+ "version": "0.10",
+ "description": "A menu for setting up Bangle.js",
"tags": "tool,system",
"storage": [
- {"name":"+setting","url":"settings.json"},
- {"name":"-setting","url":"settings.js"},
- {"name":"=setting","url":"settings-init.js"},
- {"name":"@setting","url":"settings-default.json","evaluate":true},
- {"name":"*setting","url":"settings-icon.js","evaluate":true}
+ {
+ "name": "setting.app.js",
+ "url": "settings.js"
+ },
+ {
+ "name": "setting.boot.js",
+ "url": "boot.js"
+ },
+ {
+ "name": "setting.json",
+ "url": "settings-default.json",
+ "evaluate": true
+ },
+ {
+ "name": "setting.img",
+ "url": "settings-icon.js",
+ "evaluate": true
+ }
],
- "sortorder" : -2
+ "sortorder": -2
},
- { "id": "wclock",
+ {
+ "id": "alarm",
+ "name": "Default Alarm",
+ "shortName": "Alarms",
+ "icon": "app.png",
+ "version": "0.05",
+ "description": "Set and respond to alarms",
+ "tags": "tool,alarm,widget",
+ "storage": [
+ {
+ "name": "alarm.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "alarm.boot.js",
+ "url": "boot.js"
+ },
+ {
+ "name": "alarm.js",
+ "url": "alarm.js"
+ },
+ {
+ "name": "alarm.json",
+ "content": "[]"
+ },
+ {
+ "name": "alarm.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ },
+ {
+ "name": "alarm.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "wclock",
"name": "Word Clock",
"icon": "clock-word.png",
- "version":"0.02",
+ "version": "0.02",
"description": "Display Time as Text",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+wclock","url":"clock-word.json"},
- {"name":"-wclock","url":"clock-word.js"},
- {"name":"*wclock","url":"clock-word-icon.js","evaluate":true}
+ {
+ "name": "wclock.app.js",
+ "url": "clock-word.js"
+ },
+ {
+ "name": "wclock.img",
+ "url": "clock-word-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "aclock",
+ {
+ "id": "aclock",
"name": "Analog Clock",
"icon": "clock-analog.png",
- "version":"0.02",
+ "version": "0.11",
"description": "An Analog Clock",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+aclock","url":"clock-analog.json"},
- {"name":"-aclock","url":"clock-analog.js"},
- {"name":"*aclock","url":"clock-analog-icon.js","evaluate":true}
+ {
+ "name": "aclock.app.js",
+ "url": "clock-analog.js"
+ },
+ {
+ "name": "aclock.img",
+ "url": "clock-analog-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "clck3x2",
- "name": "3x2 Pixel Clock",
- "icon": "clock3x2.png",
- "version":"0.02",
- "description": "This is a simple clock using minimalistic 3x2 pixel numerical digits",
+ {
+ "id": "clock2x3",
+ "name": "2x3 Pixel Clock",
+ "icon": "clock2x3.png",
+ "version": "0.04",
+ "description": "This is a simple clock using minimalist 2x3 pixel numerical digits",
"tags": "clock",
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+clck3x2","url":"clock3x2.json"},
- {"name":"-clck3x2","url":"clock3x2.js"},
- {"name":"*clck3x2","url":"clock3x2-icon.js","evaluate":true}
+ {
+ "name": "clock2x3.app.js",
+ "url": "clock2x3-app.js"
+ },
+ {
+ "name": "clock2x3.img",
+ "url": "clock2x3-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "trex",
+ {
+ "id": "trex",
"name": "T-Rex",
"icon": "trex.png",
- "version":"0.01",
+ "version": "0.01",
"description": "T-Rex game in the style of Chrome's offline game",
"tags": "game",
- "allow_emulator":true,
+ "allow_emulator": true,
"storage": [
- {"name":"+trex","url":"trex.json"},
- {"name":"-trex","url":"trex.js"},
- {"name":"*trex","url":"trex-icon.js","evaluate":true}
+ {
+ "name": "trex.app.js",
+ "url": "trex.js"
+ },
+ {
+ "name": "trex.img",
+ "url": "trex-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "astroid",
+ {
+ "id": "astroid",
"name": "Asteroids!",
"icon": "asteroids.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Retro asteroids game",
"tags": "game",
- "allow_emulator":true,
+ "allow_emulator": true,
"storage": [
- {"name":"+astroid","url":"asteroids.json"},
- {"name":"-astroid","url":"asteroids.js"},
- {"name":"*astroid","url":"asteroids-icon.js","evaluate":true}
+ {
+ "name": "astroid.app.js",
+ "url": "asteroids.js"
+ },
+ {
+ "name": "astroid.img",
+ "url": "asteroids-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "clickms",
+ {
+ "id": "clickms",
"name": "Click Master",
"icon": "click-master.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Get several friends to start the game, then compete to see who can press BTN1 the most!",
"tags": "game",
"storage": [
- {"name":"+clickms","url":"click-master.json"},
- {"name":"-clickms","url":"click-master.js"},
- {"name":"*clickms","url":"click-master-icon.js","evaluate":true}
+ {
+ "name": "clickms.app.js",
+ "url": "click-master.js"
+ },
+ {
+ "name": "clickms.img",
+ "url": "click-master-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "horsey",
+ {
+ "id": "horsey",
"name": "Horse Race!",
"icon": "horse-race.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Get several friends to start the game, then compete to see who can press BTN1 the most!",
"tags": "game",
"storage": [
- {"name":"+horsey","url":"horse-race.json"},
- {"name":"-horsey","url":"horse-race.js"},
- {"name":"*horsey","url":"horse-race-icon.js","evaluate":true}
+ {
+ "name": "horsey.app.js",
+ "url": "horse-race.js"
+ },
+ {
+ "name": "horsey.img",
+ "url": "horse-race-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "compass",
+ {
+ "id": "compass",
"name": "Compass",
"icon": "compass.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Simple compass that points North",
"tags": "tool,outdoors",
"storage": [
- {"name":"+compass","url":"compass.json"},
- {"name":"-compass","url":"compass.js"},
- {"name":"*compass","url":"compass-icon.js","evaluate":true}
+ {
+ "name": "compass.app.js",
+ "url": "compass.js"
+ },
+ {
+ "name": "compass.img",
+ "url": "compass-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "gpstime",
+ {
+ "id": "gpstime",
"name": "GPS Time",
"icon": "gpstime.png",
- "version":"0.01",
+ "version": "0.03",
"description": "Update the Bangle.js's clock based on the time from the GPS receiver",
- "tags": "tool",
+ "tags": "tool,gps",
"storage": [
- {"name":"+gpstime","url":"gpstime.json"},
- {"name":"-gpstime","url":"gpstime.js"},
- {"name":"*gpstime","url":"gpstime-icon.js","evaluate":true}
+ {
+ "name": "gpstime.app.js",
+ "url": "gpstime.js"
+ },
+ {
+ "name": "gpstime.img",
+ "url": "gpstime-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "openloc",
+ {
+ "id": "openloc",
"name": "Open Location / Plus Codes",
- "icon": "openlocation.png",
- "version":"0.01",
+ "shortName": "Open Location",
+ "icon": "app.png",
+ "version": "0.01",
"description": "Convert your current GPS location to a series of characters",
- "tags": "tool,outdoors",
+ "tags": "tool,outdoors,gps",
"storage": [
- {"name":"+openloc","url":"openlocation.json"},
- {"name":"-openloc","url":"openlocation.js","evaluate":true}
+ {
+ "name": "openloc.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "openloc.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "speedo",
+ {
+ "id": "speedo",
"name": "Speedo",
"icon": "speedo.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Show the current speed according to the GPS",
- "tags": "tool,outdoors",
+ "tags": "tool,outdoors,gps",
"storage": [
- {"name":"+speedo","url":"speedo.json"},
- {"name":"-speedo","url":"speedo.js"},
- {"name":"*speedo","url":"speedo-icon.js","evaluate":true}
+ {
+ "name": "speedo.app.js",
+ "url": "speedo.js"
+ },
+ {
+ "name": "speedo.img",
+ "url": "speedo-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "slevel",
+ {
+ "id": "gpsrec",
+ "name": "GPS Recorder",
+ "icon": "app.png",
+ "version": "0.06",
+ "interface": "interface.html",
+ "description": "Application that allows you to record a GPS track. Can run in background",
+ "tags": "tool,outdoors,gps,widget",
+ "storage": [
+ {
+ "name": "gpsrec.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "gpsrec.json",
+ "url": "app-settings.json",
+ "evaluate": true
+ },
+ {
+ "name": "gpsrec.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ },
+ {
+ "name": "gpsrec.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "heart",
+ "name": "Heart Rate Recorder",
+ "icon": "app.png",
+ "version": "0.01",
+ "interface": "interface.html",
+ "description": "Application that allows you to record your heart rate. Can run in background",
+ "tags": "tool,health,widget",
+ "storage": [
+ {
+ "name": "heart.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "heart.json",
+ "url": "app-settings.json",
+ "evaluate": true
+ },
+ {
+ "name": "heart.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ },
+ {
+ "name": "heart.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "slevel",
"name": "Spirit Level",
"icon": "spiritlevel.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Show the current angle of the watch, so you can use it to make sure something is absolutely flat",
"tags": "tool",
"storage": [
- {"name":"+slevel","url":"spiritlevel.json"},
- {"name":"-slevel","url":"spiritlevel.js"},
- {"name":"*slevel","url":"spiritlevel-icon.js","evaluate":true}
+ {
+ "name": "slevel.app.js",
+ "url": "spiritlevel.js"
+ },
+ {
+ "name": "slevel.img",
+ "url": "spiritlevel-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "files",
+ {
+ "id": "files",
"name": "App Manager",
"icon": "files.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Show currently installed apps, free space, and allow their deletion from the watch",
"tags": "tool,system",
"storage": [
- {"name":"+files","url":"files.json"},
- {"name":"-files","url":"files.js"},
- {"name":"*files","url":"files-icon.js","evaluate":true}
+ {
+ "name": "files.app.js",
+ "url": "files.js"
+ },
+ {
+ "name": "files.img",
+ "url": "files-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "sbat",
+ {
+ "id": "widbat",
"name": "Battery Level Widget",
- "icon": "widget-battery.png",
- "version":"0.01",
+ "icon": "widget.png",
+ "version": "0.04",
"description": "Show the current battery level and charging status in the top right of the clock",
"tags": "widget,battery",
- "type":"widget",
+ "type": "widget",
"storage": [
- {"name":"+sbat","url":"widget-battery.json"},
- {"name":"=sbat","url":"widget-battery.js"}
+ {
+ "name": "widbat.wid.js",
+ "url": "widget.js"
+ }
]
},
- { "id": "sbt",
+ {
+ "id": "widbatpc",
+ "name": "Battery Level Widget (with percentage)",
+ "shortName": "Battery Widget",
+ "icon": "widget.png",
+ "version": "0.08",
+ "description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
+ "tags": "widget,battery",
+ "type": "widget",
+ "storage": [
+ {
+ "name": "widbatpc.wid.js",
+ "url": "widget.js"
+ },
+ {
+ "name": "widbatpc.settings.js",
+ "url": "settings.js"
+ },
+ {
+ "name": "widbatpc.settings.json",
+ "content": "{}"
+ }
+ ]
+ },
+ {
+ "id": "widbt",
"name": "Bluetooth Widget",
- "icon": "widget-bluetooth.png",
- "version":"0.01",
+ "icon": "widget.png",
+ "version": "0.03",
"description": "Show the current Bluetooth connection status in the top right of the clock",
"tags": "widget,bluetooth",
- "type":"widget",
+ "type": "widget",
"storage": [
- {"name":"+sbt","url":"widget-bluetooth.json"},
- {"name":"=sbt","url":"widget-bluetooth.js"}
+ {
+ "name": "widbt.wid.js",
+ "url": "widget.js"
+ }
]
},
- { "id": "hrm",
+ {
+ "id": "hrm",
"name": "Heart Rate Monitor",
"icon": "heartrate.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Measure your current heart rate",
"tags": "health",
"storage": [
- {"name":"+hrm","url":"heartrate.json"},
- {"name":"-hrm","url":"heartrate.js"},
- {"name":"*hrm","url":"heartrate-icon.js","evaluate":true}
+ {
+ "name": "hrm.app.js",
+ "url": "heartrate.js"
+ },
+ {
+ "name": "hrm.img",
+ "url": "heartrate-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "stetho",
+ {
+ "id": "widhrm",
+ "name": "Simple Heart Rate widget",
+ "icon": "widget.png",
+ "version": "0.03",
+ "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.",
+ "tags": "health,widget",
+ "type": "widget",
+ "storage": [
+ {
+ "name": "widhrm.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "stetho",
"name": "Stethoscope",
"icon": "stetho.png",
- "version":"0.0198",
+ "version": "0.01",
"description": "Hear your heart rate",
"tags": "health",
"storage": [
- {"name":"+stetho","url":"stetho.json"},
- {"name":"-stetho","url":"stetho.js"},
- {"name":"*stetho","url":"stetho-icon.js","evaluate":true}
+ {
+ "name": "stetho.app.js",
+ "url": "stetho.js"
+ },
+ {
+ "name": "stetho.img",
+ "url": "stetho-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "swatch",
+ {
+ "id": "swatch",
"name": "Stopwatch",
"icon": "stopwatch.png",
- "version":"0.01",
- "description": "Simple stopwatch with Lap Time recording",
+ "version": "0.05",
+ "interface": "interface.html",
+ "description": "Simple stopwatch with Lap Time logging to a JSON file",
"tags": "health",
- "allow_emulator":true,
+ "allow_emulator": true,
+ "readme": "README.md",
"storage": [
- {"name":"+swatch","url":"stopwatch.json"},
- {"name":"-swatch","url":"stopwatch.js"},
- {"name":"*swatch","url":"stopwatch-icon.js","evaluate":true}
+ {
+ "name": "swatch.app.js",
+ "url": "stopwatch.js"
+ },
+ {
+ "name": "swatch.img",
+ "url": "stopwatch-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "hidmsic",
+ {
+ "id": "hidmsic",
"name": "Bluetooth Music Controls",
+ "shortName": "Music Control",
"icon": "hid-music.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Enable HID in settings, pair with your phone, then use this app to control music from your watch!",
"tags": "bluetooth",
"storage": [
- {"name":"+hidmsic","url":"hid-music.json"},
- {"name":"-hidmsic","url":"hid-music.js"},
- {"name":"*hidmsic","url":"hid-music-icon.js","evaluate":true}
+ {
+ "name": "hidmsic.app.js",
+ "url": "hid-music.js"
+ },
+ {
+ "name": "hidmsic.img",
+ "url": "hid-music-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "hidkbd",
+ {
+ "id": "hidkbd",
"name": "Bluetooth Keyboard",
+ "shortName": "Bluetooth Kbd",
"icon": "hid-keyboard.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Enable HID in settings, pair with your phone/PC, then use this app to control other apps",
"tags": "bluetooth",
"storage": [
- {"name":"+hidkbd","url":"hid-keyboard.json"},
- {"name":"-hidkbd","url":"hid-keyboard.min.js"},
- {"name":"*hidkbd","url":"hid-keyboard-icon.js","evaluate":true}
+ {
+ "name": "hidkbd.app.js",
+ "url": "hid-keyboard.js"
+ },
+ {
+ "name": "hidkbd.img",
+ "url": "hid-keyboard-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "hidbkbd",
+ {
+ "id": "hidbkbd",
"name": "Binary Bluetooth Keyboard",
+ "shortName": "Binary BT Kbd",
"icon": "hid-binary-keyboard.png",
- "version":"0.01",
+ "version": "0.01",
"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",
"tags": "bluetooth",
"storage": [
- {"name":"+hidbkbd","url":"hid-binary-keyboard.json"},
- {"name":"-hidbkbd","url":"hid-binary-keyboard.js"},
- {"name":"*hidbkbd","url":"hid-binary-keyboard-icon.js","evaluate":true}
+ {
+ "name": "hidbkbd.app.js",
+ "url": "hid-binary-keyboard.js"
+ },
+ {
+ "name": "hidbkbd.img",
+ "url": "hid-binary-keyboard-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "animals",
+ {
+ "id": "animals",
"name": "Animals Game",
"icon": "animals.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Simple toddler's game - displays a different number of animals each time the screen is pressed",
"tags": "game",
"storage": [
- {"name":"+animals","url":"animals.json"},
- {"name":"-animals","url":"animals.js"},
- {"name":"*animals","url":"animals-icon.js","evaluate":true},
- {"name":"*snake","url":"animals-snake.js","evaluate":true},
- {"name":"*duck","url":"animals-duck.js","evaluate":true},
- {"name":"*swan","url":"animals-swan.js","evaluate":true},
- {"name":"*fox","url":"animals-fox.js","evaluate":true},
- {"name":"*camel","url":"animals-camel.js","evaluate":true},
- {"name":"*pig","url":"animals-pig.js","evaluate":true},
- {"name":"*sheep","url":"animals-sheep.js","evaluate":true},
- {"name":"*mouse","url":"animals-mouse.js","evaluate":true}
- ],
- "sortorder" : 1
+ {
+ "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",
+ {
+ "id": "qrcode",
"name": "Custom QR Code",
"icon": "qrcode.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Use this to upload a customised QR code to Bangle.js",
"tags": "",
"custom": "qrcode.html",
"storage": [
- {"name":"-qrcode"},
- {"name":"+qrcode"},
- {"name":"=qrcode"}
+ {
+ "name": "qrcode.app.js"
+ },
+ {
+ "name": "qrcode.img"
+ }
]
},
- { "id": "beer",
+ {
+ "id": "beer",
"name": "Beer Compass",
"icon": "beercompass.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one",
"tags": "",
"custom": "beercompass.html",
"storage": [
- {"name":"-beer"},
- {"name":"+beer"},
- {"name":"=beer"}
+ {
+ "name": "beer.app.js"
+ },
+ {
+ "name": "beer.img"
+ }
]
},
- { "id": "route",
+ {
+ "id": "route",
"name": "Route Viewer",
"icon": "route.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Upload a KML file of a route, and have your watch display a map with how far around it you are",
"tags": "",
"custom": "route.html",
"storage": [
- {"name":"-route"},
- {"name":"+route"},
- {"name":"=route"}
+ {
+ "name": "route.app.js"
+ },
+ {
+ "name": "route.img"
+ }
]
},
-
-
-
{
"id": "ncstart",
"name": "NCEU Startup",
"icon": "start.png",
- "version":"0.02",
+ "version": "0.03",
"description": "NodeConfEU 2019 'First Start' Sequence",
- "tags": "start",
+ "tags": "start,welcome",
"storage": [
- {"name":"+ncstart","url":"start.json"},
- {"name":".boot3","url":"start.js"},
- {"name":"*ncstart","url":"start-icon.js","evaluate":true},
- {"name":"*bangle","url":"start-bangle.js","evaluate":true},
- {"name":"*nceu","url":"start-nceu.js","evaluate":true},
- {"name":"*nfr","url":"start-nfr.js","evaluate":true},
- {"name":"*nodew","url":"start-nodew.js","evaluate":true},
- {"name":"*tf","url":"start-tf.js","evaluate":true}
- ],
- "sortorder" : -1
+ {
+ "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
+ }
+ ]
},
- { "id": "ncfrun",
+ {
+ "id": "ncfrun",
"name": "NCEU 5K Fun Run",
"icon": "nceu-funrun.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Display a map of the NodeConf EU 2019 5K Fun Run route and your location on it",
"tags": "health",
"storage": [
- {"name":"+ncfrun","url":"nceu-funrun.json"},
- {"name":"-ncfrun","url":"nceu-funrun.js"},
- {"name":"*ncfrun","url":"nceu-funrun-icon.js","evaluate":true}
- ],
- "sortorder" : -1
+ {
+ "name": "ncfrun.app.js",
+ "url": "nceu-funrun.js"
+ },
+ {
+ "name": "ncfrun.img",
+ "url": "nceu-funrun-icon.js",
+ "evaluate": true
+ }
+ ]
},
- { "id": "nceuwid",
+ {
+ "id": "widnceu",
"name": "NCEU Logo Widget",
- "icon": "nceu-widget.png",
- "version":"0.01",
+ "icon": "widget.png",
+ "version": "0.02",
"description": "Show the NodeConf EU logo in the top left",
"tags": "widget",
- "type":"widget",
+ "type": "widget",
"storage": [
- {"name":"+nceuwid","url":"nceu-widget.json"},
- {"name":"=nceuwid","url":"nceu-widget.js"}
- ],
- "sortorder" : -1
- },
-
-
-
- { "id": "sclock",
- "name": "Simple 24hr Clock",
- "icon": "clock-simple.png",
- "version":"0.03",
- "description": "Simple Digital 24 Hour Clock",
- "tags": "clock",
- "type":"clock",
- "allow_emulator":true,
- "storage": [
- {"name":"+sclock","url":"clock-simple.json"},
- {"name":"-sclock","url":"clock-simple.js"},
- {"name":"*sclock","url":"clock-simple-icon.js","evaluate":true}
+ {
+ "name": "widnceu.wid.js",
+ "url": "widget.js"
+ }
]
},
- { "id": "stclck",
- "name": "Simple 12hr Clock",
+ {
+ "id": "sclock",
+ "name": "Simple Clock",
"icon": "clock-simple.png",
- "version":"0.03",
- "description": "Simple Digital 12 Hour Clock",
+ "version": "0.04",
+ "description": "A Simple Digital Clock",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+stclck","url":"clock-simple.json"},
- {"name":"-stclck","url":"clock-simple.js"},
- {"name":"*stclck","url":"clock-simple-icon.js","evaluate":true}
+ {
+ "name": "sclock.app.js",
+ "url": "clock-simple.js"
+ },
+ {
+ "name": "sclock.img",
+ "url": "clock-simple-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "gesture",
+ {
+ "id": "dclock",
+ "name": "Dev Clock",
+ "icon": "clock-dev.png",
+ "version": "0.09",
+ "description": "A Digital Clock including timestamp (tst), beats(@), days in current month (dm) and days since new moon (l)",
+ "tags": "clock",
+ "type": "clock",
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "dclock.app.js",
+ "url": "clock-dev.js"
+ },
+ {
+ "name": "dclock.img",
+ "url": "clock-dev-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "gesture",
"name": "Gesture Test",
"icon": "gesture.png",
- "version":"0.01",
+ "version": "0.01",
"description": "BETA! Uploads a basic Tensorflow Gesture model, and then outputs each gesture as a message",
"tags": "gesture,ai",
- "type":"app",
+ "type": "app",
"storage": [
- {"name":"+gesture","url":"gesture.json"},
- {"name":"-gesture","url":"gesture.js"},
- {"name":".tfnames","url":"gesture-tfnames.js","evaluate":true},
- {"name":".tfmodel","url":"gesture-tfmodel.js","evaluate":true},
- {"name":"*gesture","url":"gesture-icon.js","evaluate":true}
+ {
+ "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",
+ {
+ "id": "pparrot",
"name": "Party Parrot",
"icon": "party-parrot.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Party with a parrot on your wrist",
"tags": "party,parrot,lol",
- "type":"app",
- "allow_emulator":true,
+ "type": "app",
+ "allow_emulator": true,
"storage": [
- {"name":"+pparrot","url":"party-parrot.json"},
- {"name":"-pparrot","url":"party-parrot.js"},
- {"name":"*pparrot","url":"party-parrot-icon.js","evaluate":true}
+ {
+ "name": "pparrot.app.js",
+ "url": "party-parrot.js"
+ },
+ {
+ "name": "pparrot.img",
+ "url": "party-parrot-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "hrings",
+ {
+ "id": "hrings",
"name": "Hypno Rings",
"icon": "hypno-rings.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Experiment with trippy rings, press buttons for change",
"tags": "rings,hypnosis,psychadelic",
- "type":"app",
- "allow_emulator":true,
+ "type": "app",
+ "allow_emulator": true,
"storage": [
- {"name":"+hrings","url":"hypno-rings.json"},
- {"name":"-hrings","url":"hypno-rings.js"},
- {"name":"*hrings","url":"hypno-rings-icon.js","evaluate":true}
+ {
+ "name": "hrings.app.js",
+ "url": "hypno-rings.js"
+ },
+ {
+ "name": "hrings.img",
+ "url": "hypno-rings-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "morse",
+ {
+ "id": "morse",
"name": "Morse Code",
"icon": "morse-code.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Learn morse code by hearing/seeing/feeling the code. Tap to toggle buzz!",
"tags": "morse,sound,visual,input",
- "type":"app",
+ "type": "app",
"storage": [
- {"name":"+morse","url":"morse-code.json"},
- {"name":"-morse","url":"morse-code.js"},
- {"name":"*morse","url":"morse-code-icon.js","evaluate":true}
+ {
+ "name": "morse.app.js",
+ "url": "morse-code.js"
+ },
+ {
+ "name": "morse.img",
+ "url": "morse-code-icon.js",
+ "evaluate": true
+ }
]
},
{
"id": "blescan",
"name": "BLE Scanner",
"icon": "blescan.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Scan for advertising BLE devices",
- "tags" : "bluetooth",
- "storage" : [
- {"name":"+blescan","url":"blescan.json"},
- {"name":"-blescan","url":"blescan.js"},
- {"name":"*blescan","url":"blescan-icon.js", "evaluate":true}
+ "tags": "bluetooth",
+ "storage": [
+ {
+ "name": "blescan.app.js",
+ "url": "blescan.js"
+ },
+ {
+ "name": "blescan.img",
+ "url": "blescan-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "mmonday",
- "name": "Manic Monday Tone",
- "icon": "manic-monday-icon.png",
- "version":"0.01",
- "description": "The Bangles make a comeback",
- "tags": "sound",
- "storage": [
- {"name":"+mmonday","url":"manic-monday.json"},
- {"name":"-mmonday","url":"manic-monday.js"},
- {"name":"*mmonday","url":"manic-monday-icon.js","evaluate":true}
- ]
+ {
+ "id": "mmonday",
+ "name": "Manic Monday Tone",
+ "icon": "manic-monday-icon.png",
+ "version": "0.02",
+ "description": "The Bangles make a comeback",
+ "tags": "sound",
+ "storage": [
+ {
+ "name": "mmonday.app.js",
+ "url": "manic-monday.js"
+ },
+ {
+ "name": "mmonday.img",
+ "url": "manic-monday-icon.js",
+ "evaluate": true
+ }
+ ]
},
- { "id": "jbells",
+ {
+ "id": "jbells",
"name": "Jingle Bells",
"icon": "jbells.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Play Jingle Bells",
"tags": "sound",
- "type":"app",
+ "type": "app",
"storage": [
- {"name":"+jbells","url":"jbells.json"},
- {"name":"-jbells","url":"jbells.js"},
- {"name":"*jbells","url":"jbells-icon.js","evaluate":true}
+ {
+ "name": "jbells.app.js",
+ "url": "jbells.js"
+ },
+ {
+ "name": "jbells.img",
+ "url": "jbells-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "scolor",
+ {
+ "id": "scolor",
"name": "Show Color",
"icon": "show-color.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Display all available Colors and Names",
"tags": "tool",
- "type":"app",
- "allow_emulator":true,
+ "type": "app",
+ "allow_emulator": true,
"storage": [
- {"name":"+scolor","url":"show-color.json"},
- {"name":"-scolor","url":"show-color.js"},
- {"name":"*scolor","url":"show-color-icon.js","evaluate":true}
+ {
+ "name": "scolor.app.js",
+ "url": "show-color.js"
+ },
+ {
+ "name": "scolor.img",
+ "url": "show-color-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "miclock",
+ {
+ "id": "miclock",
"name": "Mixed Clock",
"icon": "clock-mixed.png",
- "version":"0.02",
+ "version": "0.04",
"description": "A mix of analog and digital Clock",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+miclock","url":"clock-mixed.json"},
- {"name":"-miclock","url":"clock-mixed.js"},
- {"name":"*miclock","url":"clock-mixed-icon.js","evaluate":true}
+ {
+ "name": "miclock.app.js",
+ "url": "clock-mixed.js"
+ },
+ {
+ "name": "miclock.img",
+ "url": "clock-mixed-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "bclock",
+ {
+ "id": "bclock",
"name": "Binary Clock",
"icon": "clock-binary.png",
- "version":"0.02",
+ "version": "0.02",
"description": "A simple binary clock watch face",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+bclock","url":"clock-binary.json"},
- {"name":"-bclock","url":"clock-binary.js"},
- {"name":"*bclock","url":"clock-binary-icon.js","evaluate":true}
+ {
+ "name": "bclock.app.js",
+ "url": "clock-binary.js"
+ },
+ {
+ "name": "bclock.img",
+ "url": "clock-binary-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "clotris",
+ {
+ "id": "clotris",
"name": "Clock-Tris",
"icon": "clock-tris.png",
- "version":"0.01",
+ "version": "0.01",
"description": "A fully functional clone of a classic game of falling blocks",
"tags": "game",
- "allow_emulator":true,
+ "allow_emulator": true,
"storage": [
- {"name":"+clotris","url":"clock-tris.json"},
- {"name":"-clotris","url":"clock-tris.js"},
- {"name":"*clotris","url":"clock-tris-icon.js","evaluate":true},
- {"name":".trishig","url":"clock-tris-high"}
+ {
+ "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",
+ {
+ "id": "flappy",
"name": "Flappy Bird",
"icon": "app.png",
- "version":"0.03",
+ "version": "0.03",
"description": "A Flappy Bird game clone",
"tags": "game",
- "allow_emulator":true,
+ "allow_emulator": true,
"storage": [
- {"name":"+flappy","url":"app.json"},
- {"name":"-flappy","url":"app.js"},
- {"name":"*flappy","url":"app-icon.js","evaluate":true}
+ {
+ "name": "flappy.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "flappy.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
]
},
{
"id": "gpsinfo",
"name": "GPS Info",
"icon": "gps-info.png",
- "version":"0.01",
+ "version": "0.02",
"description": "An application that displays information about altitude, lat/lon, satellites and time",
"tags": "gps",
"type": "app",
"storage": [
- {"name": "+gpsinfo","url": "gps-info.json"},
- {"name": "-gpsinfo","url": "gps-info.js"},
- {"name": "*gpsinfo","url": "gps-info-icon.js","evaluate": true}
+ {
+ "name": "gpsinfo.app.js",
+ "url": "gps-info.js"
+ },
+ {
+ "name": "gpsinfo.img",
+ "url": "gps-info-icon.js",
+ "evaluate": true
+ }
]
},
{
"id": "pomodo",
- "name":"Pomodoro",
- "icon":"pomodoro.png",
- "version":"0.01",
+ "name": "Pomodoro",
+ "icon": "pomodoro.png",
+ "version": "0.01",
"description": "A simple pomodoro timer.",
"tags": "pomodoro,cooking,tools",
"type": "app",
- "allow_emulator":true,
+ "allow_emulator": true,
"storage": [
- {"name": "+pomodo","url": "pomodoro.json"},
- {"name": "-pomodo","url": "pomodoro.js"},
- {"name": "*pomodo","url": "pomodoro-icon.js","evaluate": true}
+ {
+ "name": "pomodo.app.js",
+ "url": "pomodoro.js"
+ },
+ {
+ "name": "pomodo.img",
+ "url": "pomodoro-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "blobclk",
+ {
+ "id": "blobclk",
"name": "Large Digit Blob Clock",
+ "shortName": "Blob Clock",
"icon": "clock-blob.png",
- "version":"0.03",
+ "version": "0.03",
"description": "A clock with big digits",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+blobclk","url":"clock-blob.json"},
- {"name":"-blobclk","url":"clock-blob.js"},
- {"name":"*blobclk","url":"clock-blob-icon.js","evaluate":true}
+ {
+ "name": "blobclk.app.js",
+ "url": "clock-blob.js"
+ },
+ {
+ "name": "blobclk.img",
+ "url": "clock-blob-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "boldclk",
+ {
+ "id": "boldclk",
"name": "Bold Clock",
"icon": "bold_clock.png",
- "version":"0.02",
+ "version": "0.02",
"description": "Simple, readable and practical clock",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+boldclk","url":"bold_clock.json"},
- {"name":"-boldclk","url":"bold_clock.js"},
- {"name":"*boldclk","url":"bold_clock-icon.js","evaluate":true}
+ {
+ "name": "boldclk.app.js",
+ "url": "bold_clock.js"
+ },
+ {
+ "name": "boldclk.img",
+ "url": "bold_clock-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "wdclk",
+ {
+ "id": "widclk",
"name": "Digital clock widget",
- "icon": "digital_clock_widget.png",
- "version":"0.01",
+ "icon": "widget.png",
+ "version": "0.03",
"description": "A simple digital clock widget",
"tags": "widget,clock",
- "type":"widget",
+ "type": "widget",
"storage": [
- {"name":"+wdclk","url":"digital_clock_widget.json"},
- {"name":"=wdclk","url":"digital_clock_widget.js"}
+ {
+ "name": "widclk.wid.js",
+ "url": "widget.js"
+ }
]
},
- { "id": "wpedom",
+ {
+ "id": "widpedom",
"name": "Pedometer widget",
- "icon": "pedometer_widget.png",
- "version":"0.01",
+ "icon": "widget.png",
+ "version": "0.08",
"description": "Daily pedometer widget",
"tags": "widget",
- "type":"widget",
+ "type": "widget",
"storage": [
- {"name":"+wpedom","url":"pedometer_widget.json"},
- {"name":"=wpedom","url":"pedometer_widget.js"}
+ {
+ "name": "widpedom.wid.js",
+ "url": "widget.js"
+ }
]
},
- { "id": "berlinc",
+ {
+ "id": "berlinc",
"name": "Berlin Clock",
"icon": "berlin-clock.png",
- "version":"0.02",
+ "version": "0.02",
"description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+berlinc","url":"berlin-clock.json"},
- {"name":"-berlinc","url":"berlin-clock.js"},
- {"name":"*berlinc","url":"berlin-clock-icon.js","evaluate":true}
+ {
+ "name": "berlinc.app.js",
+ "url": "berlin-clock.js"
+ },
+ {
+ "name": "berlinc.img",
+ "url": "berlin-clock-icon.js",
+ "evaluate": true
+ }
]
},
- { "id": "ctrclk",
+ {
+ "id": "ctrclk",
"name": "Centerclock",
"icon": "app.png",
- "version":"0.02",
+ "version": "0.02",
"description": "Watch-centered digital 24h clock with date in dd.mm.yyyy format.",
"tags": "clock",
- "type":"clock",
- "allow_emulator":true,
+ "type": "clock",
+ "allow_emulator": true,
"storage": [
- {"name":"+ctrclk","url":"app.json"},
- {"name":"-ctrclk","url":"app.js"},
- {"name":"*ctrclk","url":"app-icon.js","evaluate":true}
- ],
- "sortorder" : -9
+ {
+ "name": "ctrclk.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "ctrclk.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
},
- { "id": "demoapp",
+ {
+ "id": "demoapp",
"name": "Demo Loop",
"icon": "app.png",
- "version":"0.01",
+ "version": "0.01",
"description": "Simple demo app - displays Bangle.js, JS logo, graphics, and Bangle.js information",
"tags": "",
- "type":"app",
- "allow_emulator":true,
+ "type": "app",
+ "allow_emulator": true,
"storage": [
- {"name":"+demoapp","url":"app.json"},
- {"name":"-demoapp","url":"app.js"},
- {"name":"*demoapp","url":"app-icon.js","evaluate":true}
+ {
+ "name": "demoapp.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "demoapp.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
],
- "sortorder" : -9
+ "sortorder": -9
},
{
"id": "jbm8b",
@@ -773,10 +1452,10 @@
"evaluate": true
}
],
- "allow_emulator":true,
- "type":"app",
+ "allow_emulator": true,
+ "type": "app",
"version": "0.02",
- "sortorder" : -9
+ "sortorder": -9
},
{
"id": "jbb",
@@ -799,9 +1478,441 @@
"evaluate": true
}
],
- "allow_emulator":true,
- "type":"app",
+ "allow_emulator": true,
+ "type": "app",
"version": "0.02",
- "sortorder" : -9
+ "sortorder": -9,
+ },
+ {
+ "id": "flagrse",
+ "name": "Espruino Flag Raiser",
+ "icon": "app.png",
+ "version": "0.01",
+ "description": "App to send a command to another Espruino to cause it to raise a flag",
+ "tags": "",
+ "storage": [
+ {
+ "name": "flagrse.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "flagrse.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "pipboy",
+ "name": "Pipboy",
+ "icon": "app.png",
+ "version": "0.02",
+ "description": "Pipboy themed clock",
+ "tags": "clock",
+ "type": "clock",
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "pipboy.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "pipboy.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "torch",
+ "name": "Torch",
+ "shortName": "Torch",
+ "icon": "app.png",
+ "version": "0.01",
+ "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN3 four times in quick succession to start when in normal clock mode",
+ "tags": "tool,torch",
+ "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": "wohrm",
+ "name": "Workout HRM",
+ "icon": "app.png",
+ "version": "0.06",
+ "description": "Workout heart rate monitor notifies you with a buzz if your heart rate goes above or below the set limits.",
+ "tags": "hrm,workout",
+ "type": "app",
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "wohrm.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "wohrm.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "widid",
+ "name": "Bluetooth ID Widget",
+ "icon": "widget.png",
+ "version": "0.02",
+ "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!",
+ "tags": "widget,address,mac",
+ "type": "widget",
+ "storage": [
+ {
+ "name": "widid.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "grocery",
+ "name": "Grocery",
+ "icon": "grocery.png",
+ "version": "0.01",
+ "description": "Simple grocery list - Display a list of product and track if you already put them in your cart.",
+ "tags": "tool,outdoors",
+ "type": "app",
+ "custom": "grocery.html",
+ "storage": [
+ {
+ "name": "grocery"
+ },
+ {
+ "name": "grocery.app.js"
+ },
+ {
+ "name": "grocery.img",
+ "url": "grocery-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "marioclock",
+ "name": "Mario Clock",
+ "icon": "marioclock.png",
+ "version": "0.07",
+ "description": "Animated retro Mario clock, with Gameboy style 8-bit grey-scale graphics.",
+ "tags": "clock,mario,retro",
+ "type": "clock",
+ "allow_emulator": true,
+ "readme": "README.md",
+ "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",
+ "icon": "app.png",
+ "version": "0.07",
+ "description": "Simple CLI-Styled Clock",
+ "tags": "clock,cli,command,bash,shell",
+ "type": "clock",
+ "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",
+ "icon": "widget.png",
+ "version": "0.01",
+ "description": "Display the version of the installed firmware in the top widget section.",
+ "tags": "widget,tool,system",
+ "type": "widget",
+ "storage": [
+ {
+ "name": "widver.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "barclock",
+ "name": "Bar Clock",
+ "icon": "clock-bar.png",
+ "version": "0.04",
+ "description": "A simple digital clock showing seconds as a bar",
+ "tags": "clock",
+ "type": "clock",
+ "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",
+ "icon": "clock-dot.png",
+ "version": "0.01",
+ "description": "A Minimal Dot Analog Clock",
+ "tags": "clock",
+ "type": "clock",
+ "allow_emulator": true,
+ "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",
+ "icon": "widget.png",
+ "version": "0.01",
+ "description": "Tiny blueish battery widget, vibs and changes level color when charging",
+ "tags": "widget,tool,system",
+ "type": "widget",
+ "storage": [
+ {
+ "name": "widtbat.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "chrono",
+ "name": "Chrono",
+ "shortName": "Chrono",
+ "icon": "chrono.png",
+ "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.",
+ "tags": "Tools",
+ "storage": [
+ {
+ "name": "chrono.app.js",
+ "url": "chrono.js"
+ },
+ {
+ "name": "chrono.img",
+ "url": "chrono-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "astrocalc",
+ "name": "Astrocalc",
+ "icon": "astrocalc.png",
+ "version": "0.01",
+ "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.",
+ "tags": "app,sun,moon,cycles,tool,outdoors",
+ "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",
+ "icon": "widget.png",
+ "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.",
+ "tags": "widget,tool",
+ "type": "widget",
+ "storage": [
+ {
+ "name": "widhwt.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "toucher",
+ "name": "Touch Launcher",
+ "shortName": "Menu",
+ "icon": "app.png",
+ "version": "0.04",
+ "description": "Touch enable left to right launcher.",
+ "tags": "tool,system,launcher",
+ "type": "launch",
+ "storage": [
+ {
+ "name": "toucher.app.js",
+ "url": "app.js"
+ }
+ ],
+ "sortorder": -10
+ },
+ {
+ "id": "balltastic",
+ "name": "Balltastic",
+ "icon": "app.png",
+ "version": "0.01",
+ "description": "Simple but fun ball eats dots game.",
+ "tags": "game,fun",
+ "type": "app",
+ "storage": [
+ {
+ "name": "balltastic.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "balltastic.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "rpgdice",
+ "name": "RPG dice",
+ "icon": "rpgdice.png",
+ "version": "0.01",
+ "description": "Simple RPG dice rolling app.",
+ "tags": "game,fun",
+ "type": "app",
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "rpgdice.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "rpgdice.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
+ },
+ {
+ "id": "widmp",
+ "name": "Moon Phase Widget",
+ "icon": "widget.png",
+ "version": "0.01",
+ "description": "Display the current moon phase in blueish for the northern hemisphere in eight phases",
+ "tags": "widget,tools",
+ "type": "widget",
+ "storage": [
+ {
+ "name": "widmp.wid.js",
+ "url": "widget.js"
+ }
+ ]
+ },
+ {
+ "id": "minionclk",
+ "name": "Minion clock",
+ "icon": "minionclk.png",
+ "version": "0.01",
+ "description": "Minion themed clock.",
+ "tags": "clock,minion",
+ "type": "clock",
+ "allow_emulator": true,
+ "storage": [
+ {
+ "name": "minionclk.app.js",
+ "url": "app.js"
+ },
+ {
+ "name": "minionclk.img",
+ "url": "app-icon.js",
+ "evaluate": true
+ }
+ ]
}
-]
+]
\ No newline at end of file
diff --git a/apps/_example_app/ChangeLog b/apps/_example_app/ChangeLog
new file mode 100644
index 000000000..5560f00bc
--- /dev/null
+++ b/apps/_example_app/ChangeLog
@@ -0,0 +1 @@
+0.01: New App!
diff --git a/apps/_example_app/add_to_apps.json b/apps/_example_app/add_to_apps.json
index 6c7172ec6..ca75a7bd8 100644
--- a/apps/_example_app/add_to_apps.json
+++ b/apps/_example_app/add_to_apps.json
@@ -1,12 +1,13 @@
// Create an entry in apps.json as follows:
{ "id": "7chname",
"name": "My app's human readable name",
+ "shortName":"Short Name",
"icon": "app.png",
+ "version":"0.01",
"description": "A detailed description of my great app",
"tags": "",
"storage": [
- {"name":"+7chname","url":"app.json"},
- {"name":"-7chname","url":"app.js"},
- {"name":"*7chname","url":"app-icon.js","evaluate":true}
+ {"name":"7chname.app.js","url":"app.js"},
+ {"name":"7chname.img","url":"app-icon.js","evaluate":true}
]
}
diff --git a/apps/_example_app/app.json b/apps/_example_app/app.json
deleted file mode 100644
index 65168c5a1..000000000
--- a/apps/_example_app/app.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Short Name",
- "icon":"*7chname",
- "src":"-7chname"
-}
diff --git a/apps/_example_widget/ChangeLog b/apps/_example_widget/ChangeLog
new file mode 100644
index 000000000..4c21f3ace
--- /dev/null
+++ b/apps/_example_widget/ChangeLog
@@ -0,0 +1 @@
+0.01: New Widget!
diff --git a/apps/_example_widget/add_to_apps.json b/apps/_example_widget/add_to_apps.json
index 705e504fc..5d0057960 100644
--- a/apps/_example_widget/add_to_apps.json
+++ b/apps/_example_widget/add_to_apps.json
@@ -1,11 +1,13 @@
// Create an entry in apps.json as follows:
{ "id": "7chname",
"name": "My widget's human readable name",
+ "shortName":"Short Name",
"icon": "widget.png",
+ "version":"0.01",
"description": "A detailed description of my great widget",
- "tags": "",
+ "tags": "widget",
+ "type": "widget",
"storage": [
- {"name":"+7chname","url":"widget.json"},
- {"name":"-7chname","url":"widget.js"},
+ {"name":"7chname.wid.js","url":"widget.js"}
]
}
diff --git a/apps/_example_widget/widget.js b/apps/_example_widget/widget.js
index 6818f0502..3893e3096 100644
--- a/apps/_example_widget/widget.js
+++ b/apps/_example_widget/widget.js
@@ -1,16 +1,16 @@
/* run widgets in their own function scope so they don't interfere with
currently-running apps */
(() => {
- // add the width
- var xpos = WIDGETPOS.tr-24;/**/;
- WIDGETPOS.tr-= 28;/* the widget width plus some extra pixel to keep distance to others */;
-
- // draw your widget at xpos
- function draw() {
- // add your code
- }
-
- // add your widget
- WIDGETS["mywidget"]={draw:draw};
+ function draw() {
+ g.reset(); // reset the graphics context to defaults (color/font/etc)
+ // add your code
+ g.drawString("X", this.x, this.y);
+ }
+ // add your widget
+ WIDGETS["mywidget"]={
+ area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
+ width: 28, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
+ draw:draw // called to draw the widget
+ };
})()
diff --git a/apps/_example_widget/widget.json b/apps/_example_widget/widget.json
deleted file mode 100644
index 239e18c77..000000000
--- a/apps/_example_widget/widget.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name":"widgetname", "type":"widget",
- "src":"-7chname"
-}
diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog
new file mode 100644
index 000000000..2c81c0537
--- /dev/null
+++ b/apps/about/ChangeLog
@@ -0,0 +1,4 @@
+0.01: New App!
+0.02: Update version checker for new filename type
+0.03: Actual pixels as of 5 Mar 2020
+0.04: Actual pixels as of 9 Mar 2020
diff --git a/apps/about/app-icon.js b/apps/about/app-icon.js
new file mode 100644
index 000000000..3f63f50fb
--- /dev/null
+++ b/apps/about/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AQqoAHFtovlFxQzOiEQF0QwJFwIwSFyIwIF6YuTGBQule7IvuEp150d5GBS+DSBwtO5wABGA4vUFxvIFwXO44wJF7hcEAAejYJQvYFpAwJF7ejRQgAHF7BcH44tLF47xGF6QtNF8l5vIqFA4gv/R/4vZABwv25ovudYwAHvIvfp+dFxlPFy4wHp9PvPHFo/HFwIvEFqYxHEINP43G4/H5vNAYIHBBgQuaGAgvEAA4vEFzIxDq0zh5YCAAvHh8zqwud/1lssPh+AF4+ABYIPBFroABnUPnPNFwvNnMPnQRDFzgvCh/OdgKMC5vOBIIvEGC4bESAeB5wAErqODGDIbGMAekFwekLw4wWDY9liAoBrpdEiASIFzdloIpBAAkQoITJF7aSERhQvUDhYATF/4v/F74A/AH4A5A="))
diff --git a/apps/about/app.js b/apps/about/app.js
new file mode 100644
index 000000000..dc7b0cad8
--- /dev/null
+++ b/apps/about/app.js
@@ -0,0 +1,33 @@
+var ENV = process.env;
+var MEM = process.memory();
+var s = require("Storage");
+
+g.clear(1);
+g.setFont("6x8");
+var y = 24, h=8;
+g.drawImage(require("heatshrink").decompress(atob("vE4gQZWg//AAI3Zh4dCoAd6wAd64Ad2j4d6l4dcn4dC6Adc+AdYv4dUggHG//kgN//AGB1WkDpkOAwsH/gDBgJ4CTRwdGl6RDl/0gHQgJeMDo2/AgcDIAIkBnAdRgJyCAAQdDlgdRgZPDgbWBDoUcDqMPRYcJgEfoA7Uh9AAgQ1BEgIdBngdRKQIACmBbB6AdB2gdRnoEDyB+C8tbbQVpgNAqOkAwMGyEQDoMB1AIBvgdDPYMC+H//7zBg//+fAA4OAgH//twDoMv/4WB3iyEAAPwHINvTYMAv/A/sC6BmBh/wDoP4gIuBdwayBAAP/DoMH4F4ToQSB+EPJQUOgKmDBgIABhAdFB4L7BgfAAYNwjpKChwJBTIQdDiAdFgHgAYIdDmDaCO4MD9Wq14dM+CdCDoU0nDjChyhBAAIdFsgdTZgaVDmPYLJk0LIodDaIcxcILRDSo80jiVECgUAvgDCmG0YQTRHDoTRBgLRCMwJDBnodDeAMDKoUvAIU/DocD6ELDoKRCAIM/LIcGG4PQUIKCBU4PzDoaEB/p3BFQKKCh9ADoXsKIVVqonCtVBoFQcAUKyFwghdB3IPBCwJZCAQMfEgQAL2AGFgZJBDoZgDABEMWYQJFgLwCkACB/gdLWYMCfoQAE35BEDpkH8EfdgYADl4mDl68BABazBFBA2CgK8CABcBUZP/8kBv58CAC1//4ABUQwASn4dgOxoALl4dC4AdYj4d6h4d+wAd6oAd2g4dCAwQA=")),120,y);
+g.drawString("BANGLEJS.COM",120,y-4);
+g.drawString("Powered by Espruino",0,y+=4+h);
+g.drawString("Version "+ENV.VERSION,0,y+=h);
+g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h);
+function getVersion(name,file) {
+ var j = s.readJSON(file,1);
+ var v = ("object"==typeof j)?j.version:false;
+ g.drawString(v?(name+" "+(v?"v"+v:"Unknown")):"NO "+name,0,y+=h);
+}
+getVersion("Bootloader","boot.info");
+getVersion("Launcher","launch.info");
+getVersion("Settings","setting.info");
+
+y+=h;
+g.drawString(MEM.total+" JS Variables available",0,y+=h);
+g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h);
+if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h);
+if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h);
+g.setFontAlign(0,-1);
+g.drawString(NRF.getAddress(),120,232);
+g.flip();
+
+// Pixel chooser image
+g.drawImage(require("heatshrink").decompress(atob("+FQgl+xnu8AIBwGQgHuAoN3gF/hcLgEHu943G3gHdhvdDwIBCAAV3uEAhoBBhsO90OgHgoACBh0IhP5AAQZD8Hw+GwAwXn4AECxGAh0MEAOeJAMP3+/Lw0GswGEHgMM9gCBAIX//5PBhvQ7gJBxAAB9ng8vs5nMDgOg8HnOwIBBgBHDAAfQNAJBBgBQDgF4HQfd7veKoKbBO4Pr30IEAhgBAIIAG3oJDx+AQwLBBYgR3JsABCzOQzOeO4cP4HPc4QCBPoPN4HNO4QoB9wAByDvBO4L2COwZ4Gd4UP/7vEf4LvGKoUAooDB9x3FgEQI4TwBgEIN4NpwEMXILvBO4bvD/Y3BO46eDgGdO4n8CoXw+cQh/w/kNd4fodoXJhLvCKYJ4Dhe7AYJXFwBHBUAgABewMPhvQd4bwB8FQqDvHO4YADhH4B4XM9nABQTsCAAf/awbXBO4Vmd4xED57vD+EwFgOIBoUNxv/1////5zOAy8AvPN6AQCbQIiCOIIKB7EILwZIEO4YACKYlFoB3CHIZ2CAIJHBEAToCMwLvBAArvCAAnAAALvDAIIPByA5BEQUM/n8O4TzCAAQtBhvd/X8d4YYBvwOBO4bBFO4b2D4ASELoP/d4IbGABMBiINLV4YAD9LyFO5bvCYYfPCARKBmAcDh3ud4Wt7vdDgONwF8O4Q8Bh5jCBAOPO4o0BgFAAoLcB/4UBLIgBDAAPI5DeKIQIDChcLL4IABGIOAJITvHAAkGs0HgG7AAO99p3Dhi2N43N7rLCxGHgF56AHCRwUwAYIlBhsNGoR3CqALCh54CFAXHAIg/CRAIDBIgtHGIR3D3ZhCWwXQwA1CAAMP5/M/nPMhp3BwAJGWIQ7Dgczt1pzIHCa4IABhpkBOgQACD4ZRCs1m4AyEO4IBBABUMXYYZDgEEvoRFd4TwBO5IAJ5nAFAMNTYZEBGgRiD7p0CO4nM43JmZABAIICBAAOA+HwgUgkEiGxFsAQOwGQLeBhPpz2QChEO8AoCd4R5CdwZpCNgdVqq0B7vQ7vdMQWIbYJkFAAIjBEoR3DCoOA8A3CYAOvh/wgH/d4hVBd4VAgn/eIYAGX4cAgw2DNQ2e9I0DBgxIBxGAWgS1DAAZrBLAi2DeAJwDOoLcFNQOA5jbCd4gACO4OgAgMHu4aBDokKgGIZ4LtBogABBgXw4HwhnL5lwEQRmJb4bvBO4/uIAfQKAJ3Gh7sC6/XcgR3NDwR3DA4K4CAQJ3GV4JrBCoZuBAIMK1Wg4eAhwRB91AdpENdwbwEAAkHP5D8DPoIrBQ4LvMNYICDO4z7Bd5HM5jvD4DxBd4PQGwIBCHIMAeAQAEhQIC4GIboTfGT4JcBO4TvINQV2sDvCAAw6DRZIcB+APEhoxDACJ3BBZPwAAIsDhTwDXwbvFO5LvQhnMu1wNQoABBAMOM4RqDuFwY4IUEGpKUCcYPwAQIXEAAnu9wbJBQPg+ArCcoIBBhkMMoqCBO4IVBEYfuNYsNLISHDZYkM/93CgmIOwJtBh3uAIPuNQZ3BLwsOSYuIAIOABYPex2P9+JxncZAJcCO5VgXYRPCWQQzF4AABDohHB5gACBYPeSAYAHdwcJQYfc/OQIAQZBwB2BABQMBhiBBcQcP///AoLkBgH4+DvI1GKxGoFRVmXYThFAAwNFh0PawUNxoDC95fBDAsP+AnFFox3B9vtO4LvBG47/CcofOPoYABWIJ3Cd4jYBB4NwgwFBd4LxCIoQuGdwJIBdAoAHBoixBAQMJhvdBALuBBAJ3Gh/ADQkNLwboBAQLvDZAMP54ACMoJcCsAYC5nOV4OXcgQADd4QADs8HsF2g1QSwQAE+AcGRILhD/5cHMAgEFg2AzuNV4bvFhp3C5igN73u6DQBMwIAC/4/BcgaQDhwtBy8A3ewEAjvBAAdQgoCEDYbHCLgRIBeAwMCQoKdDwEMg6XBBgIXDO4WJhuNHQyOF+DvFAAwLB9vdVg7vJAAeXhYjHhGAAIKpL6CoBd4UDgbvDO44gDAYMHW4bCECIWdOoI2FKA0A0AABAwfu9oOFOwPgPI4ABWAICBE4p3KAARaBJQQDCAgJ3DdYLsEdwm3FwP/dwRiCd4nwQoYfDxEN7uIVxh3B1R3Bh0ONo/u93gAIIfMbozvY7oFELoMwA4h3CAAMJzOQAgOIO4LvG6ENAQP4xCjDAAiBBh6aBgEKd4139xNFd4SEBAAY6BhgHExAuG3ewO4zxCTBgnBAAMAgZKCEoo9EO4QAEdAIBBO4mPx5eBuCTDCYWfh/P6AeFNgVwg53EfITvC4BIB4B3HMgv/Vw3d7p3CFIPgHAwAMG4IAROwR1BAIWI/GAhm3gHMLAUAg1md4Q/Fh3uRgN3d4o+CPQPAAAWQ/7GB5nMH48DO4xDCF4YFCP4OAwD4GJgQCBhkJJQquGAwvAAQZsBAALvChfLuAICTKGIwBSDhoEB9yEBNwMM4GfgH8hnPO4wuBmB3ChYfFTYivBhAwBfAQABuA/GVAKKCADH4xHwhm8RYSICAALNIO4vQfgZfB8Hgd5H//gqBeYIrB5fLF4gAC6ENzIQBd453FYoUPO4ZUBCQMP/5SLuHwSg5UBAoggBxCiEJoe8714zUQCYbvBO4pDFXwRPBd4UOfwIzB5e7O44ABzP/LYp3CPAIHCu4XGhgiBBwR3IRQcP54ECyEJzJ3DkYUDGIIABRQTvJhvcZghFCu4XBZgRKGbQQAEO4m7hewGIIAEEJJjIKASKDNwh3Id4cJhJ5BOoMOgE9mAQCxGAd4jBHDAMN3p2Dd4Z+FSYThHhYDCnm8AgWwPAIVB/nM9nDO5kP//wBZD+DF4kPOoIBBC4rtCLwMO8EAgchd4w6JzwYBhHdegYkBO4oMDJwxKEgcAQgZ3D5//53Onk8O4a+BAIO62DbJwEJKIMIZoa1D+AABR4X/O4jvDO4PHyEQu0GfoIADegIAB5vmwGrd4YADSYMGy2WO4jODd4j5EAA52BMwLvB53uO4MNTIUBgIRB1WgCwXuEZYABg4EDHYI9CXAK6FLQcOO4IFBsACBGoMRgGHO4mJO4IAChkKyENNoTvFKwLGHhh5BhnMPoQEDBAnM5jvB4YIBFQUQ+EQd4vgV4LuDAAI0F6DUDO5eZzIFDO4TvDGYIBBd4OHw53BxR3E4GqyHA2ArBgwJBhe7XRH/O4UAhzONAAp3Bh8B+KWBAAnu8CRCAAVVgtQAoULeAq3GABOOSwp3DBIMICg0LW4MJyEIBoTvC38vYgeQyGZBYI3BfAx/DO5wcBSoLsDEILuBhn8BQdA+FAeIw/DBAbuDuEHf4adDbgQBB4IiF2ELbwQBBAwIMDEAuy+R3DOgJ4BO4vQIwfMGQJdB5nM55rELYo4CAAXvO4cIxDdEbw5MDO4n/PAMHAAQJCg/ud4UMAAYMCzOIwB3CEwWwO4oABJQbvFAAg3BHAPgFIKpDO4TgB//5RYIABjUAhUQeAYABxAeC7qWDABJXDOwYABBAsHu7vEAwIbD5h3FhKCBd45qD7ACB1StDBwK4CXY7vGO4cJzOZznMKgoUBO4g/BLYp5MO4sNO4UODYbuCKITvB54TBd453Fd48NhADBZwSnD/7aBh7KBOYZNNhx9CAAQoCO4uIOCIbCAAaiBI4Xg8AUGaoLvB4HwO4bzB34MBhI3BhZxBd4YGBd4t3agRCI7sNAAJsDAQMMN4oKB5jvEAAUNSIhkBh7tDAIcADQuIAALMBd4YBCh0JeAZ3G93Ah7RDAAO7+EJd4QAKd4IOB9x3LOwoADOwxJB5wgBhZHEAYq3B+Hw/8AuAIBAQScBDQQBBd4RtBF4OQAALvOzJ2DRATvCzJ3McQh3BhIfCZghrH7Z3CPAZEC+P4ZwwAHh7vBh/wg4ABTgpRBAIPuEwXteAhlEAAkL3YEC/PwAgW5VoYAGFIYACJ4nMRYIxCc4vMNgUJm4MBIoR3DhxFC/8QDAYiBu7cBRIdwUwLvBAAp3DdwYlBNga3LAA7vHLIZmBBQYMEhGIAodVDwQfB7sNHAf/JgUJMIML7wGBMogACiMf/4VBhKZBuFwhgODuHQE4LwBgDvFCIO7hbNCYokNAgMLXYUPAAp4G+xPCd4vHvgSGPIbvEAAKVCGITwDUAcJ06uHEQSsFhZ3Cd4ZBCO4bqCuAJCO4ULhZ4Bd4Y7C4AqCCQQAK+B9B/9gIQ53FwBxEhAFB5ncDYIsMAA5CD8DCBAQQADd5AFB7ruCh7sBAIaQCAARMBhAzGd52ZzMAsx3CYAZFB5nMTQTMFBgOAJQPQBghYCAQJBBO5wAKIQNwg7vBO4buBABewAAK+DGime9L0DNoI2BeQXAWoZ2Ef4Z3ILAMJyG5IQKoD9wABgHN8F5f5wAGcgJ3GdocAgjuDABLvCdQcGAoh3Fh/vdIJ3CcQLbFPAgAD5ncgEKAIPdRoMJCoJCD/4CBEYIaB4HguGgKBYDGTAKBKfIYQBCQnwaoICCd49gsDKGzLvHKYQADxAIC8HuAQINDd4Wg0HQ5j4ByAaEHoTvFO4OwMouYmcwh//AIIKDhByGZgZ3Bg7dBgxoFCAWACYjoDh7uBgwGDBocN5YfFhz1Bg4GCxOAd5B3BOILwBd4PMZJQAOxEwRoJFCqACBxw3DAASEEd4I7BAwQ4Sd46OCLQIAHO4cIH4R2BPAwAHgYIHhpODO55qBMwMI9HoeYZBC5kM4DvEZ4XAxGAg93zLeC3ew2DwFdwIFEO4kJFoRxDFoQFDBwMA8B2ChjrBAAaAFyBeBAA3QzOZOxQrBUoLvDVYXdSIR3DhnMAALvC6Hgd4YQCIAXwgELfCMPqAcCuF3O4l3AwgAF4AABIQJ3HyYCB1MK7gOCYwOQB4cMNYP/WoYMByDtBBAQHBhv9/p3FOwXMeAK6ChKMCKYV5U4Z3Bd4bqDAAZ3F81wdA14KQggEd4ZlBhn8Qg7vCyGQ6EMgF3O4LvLhQEDxEIMAOgO4MPDQJ3G553DABC4EO4zvM8HgFoQAB+CiBHoIgCAQbwFPQcAgjvHSgPQCINwvvQgEJhe7AAIbBhIWCGARrCwACBKoPd+H9DQJ3DGgPMVwfHyBwEO4ziDWoLvJCgXw9wDBO4f/gHcSYcMDwT0CAAgJDolANAPpeQgfBDQNwuDvD2CaC4HACALuEd4iRB7vzO4MIhEHJITwCZIMMvLYIgf/+RwBaoLWBAYQAHhwLBd4YACqHwAILlFAILyHPAUEAAIkBTIQAGO4QXDO4wAJdQMN7vddwOIg93XIXMhxRBdwIcJ+Hw/7iChnsBgkNhsMHoUOCAJ3BegQABgtVNQwzBAYMLWYIADO4VAOwNAd4oAEKwR3GgEJWwaREVAS6EAA4PCOA7KEO4QDBAIIjBSIPMDYxyDhaCBb4zvJ9wAE2C4CO4IAGFQPgLoVt5nODoJ3B3YTGWQhnIBQkMQoSGMAAwXCh///5/BNgJtC7q9D2HQ2G9BAT/BhLDChgfCCYYADSwZ3I93gAIJ3FABMO7wECCoJmMhkN7o2ChOQzOQcgQAD3ewKYJVFg93u9wEgp3Dd4R6CVYXA2GQgyLCfhTvHyBZCO5vvvaVBD4QkE9wRE/5mDAQR3BhoWCOgIBBAA2q0D3Md4IOMABBPDO5DvGO47YIh8O+65GNAQRF/7dFgHMd4mIwABBQoISEBAMOAAUA8DjDAA/MAYRAF7rxCABsPd5oAN995Z4mAwHM4AQF/+IO4wAGyDvFepB3BgBhCNYNwg93hGIgHAGoUHCwibDoAeDagQXBAIIRCC4h3EgxRLXQQLIhDUBO4cIhZ3Bd44AFzJxDCIMM/IxEd4kNDIsHg8IAgJ3DeAt3AoJiBRIUO9zFDJwIAB2BIJ8C2JIogMJwBBEAAMwaQoAQHBYAChruBd4QHB5iBECgzaCN4MMCQTvF35mGQYR3Ex2wAYP8O4gvG9ns8GIwEMO4cLeAQlCO4hNHAAS4CHAQaBhgACd4sOuHnd4RdDdwYBBCwK+GRIOIJALuBSQUPIQV3DIIABhGZwB3EP4UGRAjXEhp9CdQruI9x4BDIPgEwUA3YABNwQAC4GQHIOwV4QAUUIRpBAwUGKwLvCxjvGVgVwTYIfDBgJvExx3Cd4gBCAAPdpxjCHwigBhLwCBQnuUoVQHARqBAARCDhn5DQIABDIUEYAbnFABDuCAAIJEDIUM5iPKO4tAgGQMIbvGhwACdwR/Dd4MHu48Bh5oCAAkOd4cwbogEBdwgABdwLvJIAJCCdxjvEP4NgB4mIDpF3AAJBCHoZ3EBQTvDc4TwDBIh1BO4X/O44FEfgLvEO4JuHQIQoBd4Z3Gh8Pdw4ABdwqWGS5LuEADp3CBQ/uCpLvH5n5eASQBSIuIaIsP+BCOMoUIDwcIhGIO6DFDABpLEuAhC/4ABDJpXBhe7gG7dw4AC8AABaAjPIAAmgdZoDCAoX8ShIJEzOZXAetFZTDFX4f/FZHP/ieQFQgrFO4g2HTQOqEBLpBeAPAPonAAwTNBKwnvd5Pb6ADB9wACFALDBIALEGAA71C4EMVBAAMFIcLO4o0EKgMPhcz9zEKOIMMHYI8DXAcHg8AxApCIwIHBAAzvEOIUAu9wO40IO5EJzIoBd4p3Fh3dAwg7Eh6TCuDFEhxRDd4uu3QFBokEoEA9RHCY4J1BhnMHYbvCuGAvAPBeoZlBH4V3GYOOXgsOFAJNBO4YSB+/3MgPMhJLBJoUJ/JvFgcAmAHE93QOoZtBAQSKDhcIeAKHIgHA53u93qeAVAAAJWB1wRDd4wAEsEIO4MGs1mu4ABHQQCBhHIO4wDB2GwG4Pu8BRBv9/CwMM/ON6ABBd4h3KhzvEOgMHAQKeBO4TvGIwQAD5nA8Hg92u1R3BAITwEd4Z3Hg0GgGIgB2BO4d2IITvJO4ZDEKQKRCd40P/+QGwsiAwsOd4hnCOAQbBKYLuLMoJFB9w=")),0,135);
+g.flip();
diff --git a/apps/about/app.png b/apps/about/app.png
new file mode 100644
index 000000000..82549495a
Binary files /dev/null and b/apps/about/app.png differ
diff --git a/apps/aclock/ChangeLog b/apps/aclock/ChangeLog
index 7819dbe2a..98e3da8e7 100644
--- a/apps/aclock/ChangeLog
+++ b/apps/aclock/ChangeLog
@@ -1 +1,8 @@
0.02: Modified for use with new bootloader and firmware
+0.03: add hour ticks, remove timers
+0.04: add day-date display
+0.07: make date and face bigger
+0.08: make dots bigger and date more readable
+0.09: center date, remove box around it, internal refactor to remove redundant code.
+0.10: remove debug, refactor seconds to show elapsed secs each time app is displayed
+0.11: shift face down for widget area, maximize face size, 0 pad single digit date, use locale for date
diff --git a/apps/aclock/clock-analog.js b/apps/aclock/clock-analog.js
index 67061af52..7b60a728f 100644
--- a/apps/aclock/clock-analog.js
+++ b/apps/aclock/clock-analog.js
@@ -1,94 +1,151 @@
-const p = Math.PI/2;
-const PRad = Math.PI/180;
+// eliminate ide undefined errors
+let g;
+let Bangle;
-let intervalRefMin = null;
-let intervalRefSec = null;
+// http://forum.espruino.com/conversations/345155/#comment15172813
+const locale = require('locale');
+const p = Math.PI / 2;
+const pRad = Math.PI / 180;
+const faceWidth = 100; // watch face radius (240/2 - 24px for widget area)
+const widgetHeight=24+1;
+let timer = null;
+let currentDate = new Date();
+const centerX = g.getWidth() / 2;
+const centerY = (g.getWidth() / 2) + widgetHeight/2;
-let minuteDate = new Date();
-let secondDate = new Date();
-function seconds(angle, r) {
- const a = angle*PRad;
- const x = 120+Math.sin(a)*r;
- const y = 120-Math.cos(a)*r;
- g.fillRect(x-1,y-1,x+1,y+1);
-}
-function hand(angle, r1,r2) {
- const a = angle*PRad;
+const seconds = (angle) => {
+ const a = angle * pRad;
+ const x = centerX + Math.sin(a) * faceWidth;
+ const y = centerY - Math.cos(a) * faceWidth;
+
+ // if 15 degrees, make hour marker larger
+ const radius = (angle % 15) ? 2 : 4;
+ g.fillCircle(x, y, radius);
+};
+
+const hand = (angle, r1, r2) => {
+ const a = angle * pRad;
const r3 = 3;
- g.fillPoly([
- 120+Math.sin(a)*r1,
- 120-Math.cos(a)*r1,
- 120+Math.sin(a+p)*r3,
- 120-Math.cos(a+p)*r3,
- 120+Math.sin(a)*r2,
- 120-Math.cos(a)*r2,
- 120+Math.sin(a-p)*r3,
- 120-Math.cos(a-p)*r3]);
-}
-function drawAll() {
+ g.fillPoly([
+ Math.round(centerX + Math.sin(a) * r1),
+ Math.round(centerY - Math.cos(a) * r1),
+ Math.round(centerX + Math.sin(a + p) * r3),
+ Math.round(centerY - Math.cos(a + p) * r3),
+ Math.round(centerX + Math.sin(a) * r2),
+ Math.round(centerY - Math.cos(a) * r2),
+ Math.round(centerX + Math.sin(a - p) * r3),
+ Math.round(centerY - Math.cos(a - p) * r3)
+ ]);
+};
+
+const drawAll = () => {
g.clear();
- secondDate = minuteDate = new Date();
+ currentDate = new Date();
// draw hands first
onMinute();
// draw seconds
- g.setColor(0,0,0.6);
- for (let i=0;i<60;i++)
- seconds(360*i/60, 90);
+ const currentSec = currentDate.getSeconds();
+ // draw all secs
+
+ for (let i = 0; i < 60; i++) {
+ if (i > currentSec) {
+ g.setColor(0, 0, 0.6);
+ } else {
+ g.setColor(0.3, 0.3, 1);
+ }
+ seconds((360 * i) / 60);
+ }
onSecond();
-}
-function onSecond() {
- g.setColor(0,0,0.6);
- seconds(360*secondDate.getSeconds()/60, 90);
- g.setColor(1,0,0);
- secondDate = new Date();
- seconds(360*secondDate.getSeconds()/60, 90);
- g.setColor(1,1,1);
+};
-}
+const resetSeconds = () => {
+ g.setColor(0, 0, 0.6);
+ for (let i = 0; i < 60; i++) {
+ seconds((360 * i) / 60);
+ }
+};
-function onMinute() {
- g.setColor(0,0,0);
- hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -10, 50);
- hand(360*minuteDate.getMinutes()/60, -10, 82);
- minuteDate = new Date();
- g.setColor(1,1,1);
- hand(360*(minuteDate.getHours() + (minuteDate.getMinutes()/60))/12, -10, 50);
- hand(360*minuteDate.getMinutes()/60, -10, 82);
- if(minuteDate.getHours() >= 0 && minuteDate.getMinutes() === 0) {
+const onSecond = () => {
+ g.setColor(0.3, 0.3, 1);
+ seconds((360 * currentDate.getSeconds()) / 60);
+ if (currentDate.getSeconds() === 59) {
+ resetSeconds();
+ onMinute();
+ }
+ g.setColor(1, 0.7, 0.2);
+ currentDate = new Date();
+ seconds((360 * currentDate.getSeconds()) / 60);
+ g.setColor(1, 1, 1);
+};
+
+const drawDate = () => {
+ g.reset();
+ g.setColor(1, 0, 0);
+ g.setFont('6x8', 2);
+
+ const dayString = locale.dow(currentDate, true);
+ // pad left date
+ const dateString = (currentDate.getDate() < 10) ? '0' : '' + currentDate.getDate().toString();
+ const dateDisplay = `${dayString}-${dateString}`;
+ // console.log(`${dayString}|${dateString}`);
+ // center date
+ const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2;
+ const t = centerY + 37;
+ g.drawString(dateDisplay, l, t, true);
+ // console.log(l, t);
+};
+const onMinute = () => {
+ if (currentDate.getHours() === 0 && currentDate.getMinutes() === 0) {
+ g.clear();
+ resetSeconds();
+ }
+ // clear existing hands
+ g.setColor(0, 0, 0);
+ // Hour
+ hand((360 * (currentDate.getHours() + currentDate.getMinutes() / 60)) / 12, -8, faceWidth - 35);
+ // Minute
+ hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10);
+
+ // get new date, then draw new hands
+ currentDate = new Date();
+ g.setColor(1, 0.9, 0.9);
+ // Hour
+ hand((360 * (currentDate.getHours() + currentDate.getMinutes() / 60)) / 12, -8, faceWidth - 35);
+ g.setColor(1, 1, 0.9);
+ // Minute
+ hand((360 * currentDate.getMinutes()) / 60, -8, faceWidth - 10);
+ if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) {
Bangle.buzz();
}
-}
+ drawDate();
+};
-function clearTimers() {
- if(intervalRefMin) {clearInterval(intervalRefMin);}
- if(intervalRefSec) {clearInterval(intervalRefSec);}
-}
+const startTimers = () => {
+ timer = setInterval(onSecond, 1000);
+};
-function startTimers() {
- minuteDate = new Date();
- secondDate = new Date();
- intervalRefSec = setInterval(onSecond,1000);
- intervalRefMin = setInterval(onMinute,60*1000);
- drawAll();
-}
-
-Bangle.on('lcdPower',function(on) {
+Bangle.on('lcdPower', (on) => {
if (on) {
- g.clear();
- Bangle.drawWidgets();
+ // g.clear();
+ drawAll();
startTimers();
- }else {
- clearTimers();
+ Bangle.drawWidgets();
+ } else {
+ if (timer) {
+ clearInterval(timer);
+ }
}
});
g.clear();
+resetSeconds();
+startTimers();
+drawAll();
Bangle.loadWidgets();
Bangle.drawWidgets();
-drawAll();
-startTimers();
+
// Show launcher when middle button pressed
-setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
+setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
diff --git a/apps/aclock/clock-analog.json b/apps/aclock/clock-analog.json
deleted file mode 100644
index 049d47406..000000000
--- a/apps/aclock/clock-analog.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name":"Analog Clock","type":"clock",
- "icon":"*aclock",
- "src":"-aclock",
- "sortorder":-10
-}
diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog
new file mode 100644
index 000000000..be3c1513c
--- /dev/null
+++ b/apps/alarm/ChangeLog
@@ -0,0 +1,5 @@
+0.01: New App!
+0.02: Fix issues with alarm scheduling
+0.03: More alarm scheduling issues
+0.04: Tweaks for variable size widget system
+0.05: Add alarm.boot.js and move code from the bootloader
diff --git a/apps/alarm/alarm.js b/apps/alarm/alarm.js
new file mode 100644
index 000000000..7f0027bc8
--- /dev/null
+++ b/apps/alarm/alarm.js
@@ -0,0 +1,60 @@
+// Chances are boot0.js got run already and scheduled *another*
+// 'load(alarm.js)' - so let's remove it first!
+clearInterval();
+
+function formatTime(t) {
+ var hrs = 0|t;
+ var mins = Math.round((t-hrs)*60);
+ return hrs+":"+("0"+mins).substr(-2);
+}
+
+function getCurrentHr() {
+ var time = new Date();
+ return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
+}
+
+function showAlarm(alarm) {
+ var msg = formatTime(alarm.hr);
+ var buzzCount = 10;
+ if (alarm.msg)
+ msg += "\n"+alarm.msg;
+ E.showPrompt(msg,{
+ title:"ALARM!",
+ buttons : {"Sleep":true,"Ok":false} // default is sleep so it'll come back in 10 mins
+ }).then(function(sleep) {
+ buzzCount = 0;
+ if (sleep) {
+ alarm.hr += 10/60; // 10 minutes
+ } else {
+ alarm.last = (new Date()).getDate();
+ if (!alarm.rp) alarm.on = false;
+ }
+ require("Storage").write("alarm.json",JSON.stringify(alarms));
+ load();
+ });
+ function buzz() {
+ Bangle.buzz(100).then(()=>{
+ setTimeout(()=>{
+ Bangle.buzz(100).then(function() {
+ if (buzzCount--)
+ setTimeout(buzz, 3000);
+ });
+ },100);
+ });
+ }
+ buzz();
+}
+
+// Check for alarms
+var day = (new Date()).getDate();
+var hr = getCurrentHr()+10000; // get current time - 10s in future to ensure we alarm if we've started the app a tad early
+var alarms = require("Storage").readJSON("alarm.json",1)||[];
+var active = alarms.filter(a=>a.on&&(a.hra.hr-b.hr);
+ showAlarm(active[0]);
+} else {
+ // otherwise just go back to default app
+ setTimeout(load, 100);
+}
diff --git a/apps/alarm/app-icon.js b/apps/alarm/app-icon.js
new file mode 100644
index 000000000..05515e859
--- /dev/null
+++ b/apps/alarm/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkGswAhiMRCCAREAo4eHBIQLEAgwYHsIJDiwHB5gACBpIhHCoYZEGA4gFCw4ABGA4HEjgXJ4IXGAwcUB4VEmf//8zogICoJIFAodMBoNDCoIADmgJB4gXIFwXDCwoABngwFC4guB4k/CQXwh4EC+YMCC44iBp4qDC4n/+gNBC41sEIJCEC4v/GAPGC4dhXYRdFC4xhCCYIXCdQRdDC5HzegQXCsxGHC45IDCwQXCUgwXHJAIXGRogXJSIIXcOw4XIPAYXcBwv/mEDBAwXOgtQC65QGC5vzoEAJAx3Nmk/mEABIiPN+dDAQIwFC4zXGFwKRCGAjvMFwQECGAgXI4YuGGAUvAgU8C4/EFwwGCAgdMC4p4EFwobFOwoXDJAIoEAApGBC4xIEABJGHGAapEAAqNBFwwXD4heI+YuBC5BIBVQhdHIw4wD5inFS4IKCCxFmigNCokzCoMzogICoIWIsMRjgPCAA3BiMWC48RBQIXJEgMRFxAJCCw4lEC44IECooOIBAaBJKwhgIAH4ACA=="))
diff --git a/apps/alarm/app.js b/apps/alarm/app.js
new file mode 100644
index 000000000..6dd0debb1
--- /dev/null
+++ b/apps/alarm/app.js
@@ -0,0 +1,104 @@
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+var alarms = require("Storage").readJSON("alarm.json",1)||[];
+/*alarms = [
+ { on : true,
+ hr : 6.5, // hours + minutes/60
+ msg : "Eat chocolate",
+ last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day!
+ rp : true, // repeat
+ }
+];*/
+
+function formatTime(t) {
+ var hrs = 0|t;
+ var mins = Math.round((t-hrs)*60);
+ return hrs+":"+("0"+mins).substr(-2);
+}
+
+function getCurrentHr() {
+ var time = new Date();
+ return time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
+}
+
+function showMainMenu() {
+ const menu = {
+ '': { 'title': 'Alarms' },
+ 'New Alarm': ()=>editAlarm(-1)
+ };
+ alarms.forEach((alarm,idx)=>{
+ txt = (alarm.on?"on ":"off ")+formatTime(alarm.hr);
+ if (alarm.rp) txt += " (repeat)";
+ menu[txt] = function() {
+ editAlarm(idx);
+ };
+ });
+ menu['< Back'] = ()=>{load();};
+ return E.showMenu(menu);
+}
+
+function editAlarm(alarmIndex) {
+ var newAlarm = alarmIndex<0;
+ var hrs = 12;
+ var mins = 0;
+ var en = true;
+ var repeat = true;
+ if (!newAlarm) {
+ var a = alarms[alarmIndex];
+ hrs = 0|a.hr;
+ mins = Math.round((a.hr-hrs)*60);
+ en = a.on;
+ repeat = a.rp;
+ }
+ const menu = {
+ '': { 'title': 'Alarms' },
+ 'Hours': {
+ value: hrs,
+ onchange: function(v){if (v<0)v=23;if (v>23)v=0;hrs=v;this.value=v;} // no arrow fn -> preserve 'this'
+ },
+ 'Minutes': {
+ value: mins,
+ onchange: function(v){if (v<0)v=59;if (v>59)v=0;mins=v;this.value=v;} // no arrow fn -> preserve 'this'
+ },
+ 'Enabled': {
+ value: en,
+ format: v=>v?"On":"Off",
+ onchange: v=>en=v
+ },
+ 'Repeat': {
+ value: en,
+ format: v=>v?"Yes":"No",
+ onchange: v=>repeat=v
+ }
+ };
+ function getAlarm() {
+ var hr = hrs+(mins/60);
+ var day = 0;
+ // If alarm is for tomorrow not today (eg, in the past), set day
+ if (hr < getCurrentHr())
+ day = (new Date()).getDate();
+ // Save alarm
+ return {
+ on : en, hr : hr,
+ last : day, rp : repeat
+ };
+ }
+ if (newAlarm) {
+ menu["> New Alarm"] = function() {
+ alarms.push(getAlarm());
+ require("Storage").write("alarm.json",JSON.stringify(alarms));
+ showMainMenu();
+ };
+ } else {
+ menu["> Save"] = function() {
+ alarms[alarmIndex] = getAlarm();
+ require("Storage").write("alarm.json",JSON.stringify(alarms));
+ showMainMenu();
+ };
+ }
+ menu['< Back'] = showMainMenu;
+ return E.showMenu(menu);
+}
+
+showMainMenu();
diff --git a/apps/alarm/app.png b/apps/alarm/app.png
new file mode 100644
index 000000000..72f364144
Binary files /dev/null and b/apps/alarm/app.png differ
diff --git a/apps/alarm/boot.js b/apps/alarm/boot.js
new file mode 100644
index 000000000..709703bdd
--- /dev/null
+++ b/apps/alarm/boot.js
@@ -0,0 +1,24 @@
+// check for alarms
+(function() {
+ var alarms = require('Storage').readJSON('alarm.json',1)||[];
+ var time = new Date();
+ var active = alarms.filter(a=>a.on&&(a.last!=time.getDate()));
+ if (active.length) {
+ active = active.sort((a,b)=>a.hr-b.hr);
+ var hr = time.getHours()+(time.getMinutes()/60)+(time.getSeconds()/3600);
+ if (!require('Storage').read("alarm.js")) {
+ console.log("No alarm app!");
+ require('Storage').write('alarm.json',"[]")
+ } else {
+ var t = 3600000*(active[0].hr-hr);
+ if (t<1000) t=1000;
+ /* execute alarm at the correct time. We avoid execing immediately
+ since this code will get called AGAIN when alarm.js is loaded. alarm.js
+ will then clearInterval() to get rid of this call so it can proceed
+ normally. */
+ setTimeout(function() {
+ load("alarm.js");
+ },t);
+ }
+ }
+})()
diff --git a/apps/alarm/widget.js b/apps/alarm/widget.js
new file mode 100644
index 000000000..dbe91b3dd
--- /dev/null
+++ b/apps/alarm/widget.js
@@ -0,0 +1,11 @@
+(() => {
+ var alarms = require('Storage').readJSON('alarm.json',1)||[];
+ alarms = alarms.filter(alarm=>alarm.on);
+ if (!alarms.length) return; // no alarms, no widget!
+ delete alarms;
+ // add the widget
+ WIDGETS["alarm"]={area:"tl",width:24,draw:function() {
+ g.setColor(-1);
+ g.drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
+ }};
+})()
diff --git a/apps/animals/animals-camel.js b/apps/animals/animals-camel.js
index 227ea39c7..bea702739 100644
--- a/apps/animals/animals-camel.js
+++ b/apps/animals/animals-camel.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4Az4/HDDAACCywaSCpgeLBQOjzowRIxZPMBIWj0YvjNRKQSCpIWBEBq/VF6hrKdpwuKABYvXFywlDXjw5aF1gvKF0gv5F0ovvFxQxkF9ztLF8ItJF5XGAAIrfeJYtB4D7TFyQlEFwg8INgZtFF6wuCF4wAFBoPAF7ouNF8AfCF56ZFFyofEF9YgCF5z6GF9y+TC4SAFF9ZgNBgovZMAeczgRJBYovXMAtPAAIQIBYouXDAWjEYUrGBMrBYgvY/15vNVCtAADqoACCs4A/AH4A/AH4A/ABY")
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4Az4/HDDAACCywaSCpgeLBQOjzowRIxZPMBIWj0YvjNRKQSCpIWBEBq/VF6hrKdpwuKABYvXFywlDXjw5aF1gvKF0gv5F0ovvFxQxkF9ztLF8ItJF5XGAAIrfeJYtB4D7TFyQlEFwg8INgZtFF6wuCF4wAFBoPAF7ouNF8AfCF56ZFFyofEF9YgCF5z6GF9y+TC4SAFF9ZgNBgovZMAeczgRJBYovXMAtPAAIQIBYouXDAWjEYUrGBMrBYgvY/15vNVCtAADqoACCs4A/AH4A/AH4A/ABY"))
diff --git a/apps/animals/animals-pig.js b/apps/animals/animals-pig.js
index d02a9c5e4..ca4416ed4 100644
--- a/apps/animals/animals-pig.js
+++ b/apps/animals/animals-pig.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/ACXJ5PKAQfKAAPJB4nDAAIsaEwQAJ5IrCAAYbFGyBWBFyooCGxQABPAyDEABYuHGxInFAAQGGLyoIGLxQvCFyAvJBIzjKdB6OLagYwDc5K/DFyIVBAAprHF5IsTHaAuKRoSOSABwvMXyZhPF5iNfF9nJp9PGB4vh5PD3u8X86PCFwO8AAS/mGQe9zAACF/CPdF4aPD3rwmGAu9FxRghegQuKL8IvBFxYwhFx6QdFpxedXIIuQLpHJ4YdCBoQDDFQosRLxAaB4YAEIRIQDFyQvEFhYwGCQgvX/wcOCYYuWDYSmCF6CfEF6mlz96vX+z+evVPCZwABCJYAJvVVAAVPAAYTNCJoAJFwYvPCY5gUAH4A/AH4A/AH4A2")
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4A/ACXJ5PKAQfKAAPJB4nDAAIsaEwQAJ5IrCAAYbFGyBWBFyooCGxQABPAyDEABYuHGxInFAAQGGLyoIGLxQvCFyAvJBIzjKdB6OLagYwDc5K/DFyIVBAAprHF5IsTHaAuKRoSOSABwvMXyZhPF5iNfF9nJp9PGB4vh5PD3u8X86PCFwO8AAS/mGQe9zAACF/CPdF4aPD3rwmGAu9FxRghegQuKL8IvBFxYwhFx6QdFpxedXIIuQLpHJ4YdCBoQDDFQosRLxAaB4YAEIRIQDFyQvEFhYwGCQgvX/wcOCYYuWDYSmCF6CfEF6mlz96vX+z+evVPCZwABCJYAJvVVAAVPAAYTNCJoAJFwYvPCY5gUAH4A/AH4A/AH4A2"))
diff --git a/apps/animals/animals-sheep.js b/apps/animals/animals-sheep.js
index 29ab696ee..4f7603aa2 100644
--- a/apps/animals/animals-sheep.js
+++ b/apps/animals/animals-sheep.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AGesAAQuuGAQ1jFQoAKF1wwdFyQycF6wwVFi4wWEiOBq1WqoABvQHBvWALsl6p4ADF4IIBGwSNjMAJdCAAVVGwQwPXjWBFoMrF4IwOFzVWp94lV4vIwNFzS8CvGi0YFCGBSNadohdCF9gAGdsgvuGJTvkGAgABFxv+wAwcAAQsLAAVPF9lPAAOIF1tWF7qMOp4DBxAABXtIADGAWB0oupGAeAvQABwJmGAwmlwQuZAAQuCF4SYDAgRtBBoeswKsDF7gsEF4+BqovfAANWF5VWFwIvZGAf+EAYuDBooOEF8ANJF/4vREIQv7BxIvZEIwvvBwQveEIIEDF9QA/AH4A/AH4ASA=")
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AGesAAQuuGAQ1jFQoAKF1wwdFyQycF6wwVFi4wWEiOBq1WqoABvQHBvWALsl6p4ADF4IIBGwSNjMAJdCAAVVGwQwPXjWBFoMrF4IwOFzVWp94lV4vIwNFzS8CvGi0YFCGBSNadohdCF9gAGdsgvuGJTvkGAgABFxv+wAwcAAQsLAAVPF9lPAAOIF1tWF7qMOp4DBxAABXtIADGAWB0oupGAeAvQABwJmGAwmlwQuZAAQuCF4SYDAgRtBBoeswKsDF7gsEF4+BqovfAANWF5VWFwIvZGAf+EAYuDBooOEF8ANJF/4vREIQv7BxIvZEIwvvBwQveEIIEDF9QA/AH4A/AH4ASA="))
diff --git a/apps/animals/animals.js b/apps/animals/animals.js
index 38e1a4812..f92882e08 100644
--- a/apps/animals/animals.js
+++ b/apps/animals/animals.js
@@ -13,7 +13,7 @@ function next(e) {
} while (current && current==last);
g.clear();
var n = 1 + (0|(Math.random()*3.9));
- var img = require("Storage").read("*"+current);
+ var img = require("Storage").read("animals-"+current+".img");
if (n==1)
g.drawImage(img,120,120,{scale:4,rotate:Math.random()-0.5});
else
diff --git a/apps/animals/animals.json b/apps/animals/animals.json
deleted file mode 100644
index a2d24941f..000000000
--- a/apps/animals/animals.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Animals Game", "type":"app",
- "icon":"*animals",
- "src":"-animals"
-}
diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog
new file mode 100644
index 000000000..0c8adeb61
--- /dev/null
+++ b/apps/astrocalc/ChangeLog
@@ -0,0 +1 @@
+0.01: Create astrocalc app
diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js
new file mode 100644
index 000000000..318147b13
--- /dev/null
+++ b/apps/astrocalc/astrocalc-app.js
@@ -0,0 +1,348 @@
+/**
+ * Inspired by: https://www.timeanddate.com
+ */
+
+const SunCalc = require("suncalc.js");
+
+function drawMoon(phase, x, y) {
+ const moonImgFiles = [
+ "new",
+ "waxing-crescent",
+ "first-quarter",
+ "waxing-gibbous",
+ "full",
+ "waning-gibbous",
+ "last-quarter",
+ "waning-crescent",
+ ];
+
+ img = require("Storage").read(`${moonImgFiles[phase]}.img`);
+ // image width & height = 92px
+ g.drawImage(img, x - parseInt(92 / 2), y);
+}
+
+// linear interpolation between two values a and b
+// u controls amount of a/b and is in range [0.0,1.0]
+function lerp(a,b,u) {
+ return (1-u) * a + u * b;
+}
+
+function titlizeKey(key) {
+ return (key[0].toUpperCase() + key.slice(1)).match(/[A-Z][a-z]+/g).join(" ");
+}
+
+function dateToTimeString(date) {
+ const hrs = ("0" + date.getHours()).substr(-2);
+ const mins = ("0" + date.getMinutes()).substr(-2);
+ const secs = ("0" + date.getMinutes()).substr(-2);
+
+ return `${hrs}:${mins}:${secs}`;
+}
+
+function drawTitle(key) {
+ const fontHeight = 16;
+ const x = 0;
+ const x2 = g.getWidth() - 1;
+ const y = fontHeight + 26;
+ const y2 = g.getHeight() - 1;
+ const title = titlizeKey(key);
+
+ g.setFont("6x8", 2);
+ g.setFontAlign(0,-1);
+ g.drawString(title,(x+x2)/2,y-fontHeight-2);
+ g.drawLine(x,y-2,x2,y-2);
+}
+
+/**
+ * @params {Number} angle Angle of point around a radius
+ * @params {Number} radius Radius of the point to be drawn, default 2
+ * @params {Object} color Color of the point
+ * @params {Number} color.r Red 0-1
+ * @params {Number} color.g Green 0-1
+ * @params {Number} color.b Blue 0-1
+ */
+function drawPoint(angle, radius, color) {
+ const pRad = Math.PI / 180;
+ const faceWidth = 80; // watch face radius
+ const centerPx = g.getWidth() / 2;
+
+ const a = angle * pRad;
+ const x = centerPx + Math.sin(a) * faceWidth;
+ const y = centerPx - Math.cos(a) * faceWidth;
+
+ if (!radius) radius = 2;
+
+ g.setColor(color.r, color.g, color.b);
+ g.fillCircle(x, y + 20, radius);
+}
+
+function drawPoints() {
+ const startColor = {r: 140, g: 255, b: 255}; // light blue
+ const endColor = {r: 0, g: 0, b: 140}; // dark turquoise
+
+ const steps = 60;
+ const step_u = 1.0 / (steps / 2);
+ let u = 0.0;
+
+ for (let i = 0; i < steps; i++) {
+ const colR = lerp(startColor.r, endColor.r, u) / 255;
+ const colG = lerp(startColor.g, endColor.g, u) / 255;
+ const colB = lerp(startColor.b, endColor.b, u) / 255;
+ const col = {r: colR, g: colG, b: colB};
+
+ if (i >= 0 && i <= 30) {
+ u += step_u;
+ } else {
+ u -= step_u;
+ }
+
+ drawPoint((360 * i) / steps, 2, col);
+ }
+}
+
+function drawData(title, obj, startX, startY) {
+ g.clear();
+ drawTitle(title);
+
+ let xPos, yPos;
+
+ if (typeof(startX) === "undefined" || startX === null) {
+ // Center text
+ g.setFontAlign(0,-1);
+ xPos = (0 + g.getWidth() - 2) / 2;
+ } else {
+ xPos = startX;
+ }
+
+ if (typeof(startY) === "undefined") {
+ yPos = 5;
+ } else {
+ yPos = startY;
+ }
+
+ g.setFont("6x8", 1);
+
+ Object.keys(obj).forEach((key) => {
+ g.drawString(`${key}: ${obj[key]}`, xPos, yPos += 20);
+ });
+
+ g.flip();
+}
+
+function drawMoonPositionPage(gps, title) {
+ const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon);
+
+ const pageData = {
+ Azimuth: pos.azimuth.toFixed(2),
+ Altitude: pos.altitude.toFixed(2),
+ Distance: `${pos.distance.toFixed(0)} km`,
+ "Parallactic Ang": pos.parallacticAngle.toFixed(2),
+ };
+ const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
+
+ drawData(title, pageData, null, 80);
+ drawPoints();
+ drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1});
+
+ let m = setWatch(() => {
+ let m = moonIndexPageMenu(gps);
+ }, BTN3, {repeat: false, edge: "falling"});
+}
+
+function drawMoonIlluminationPage(gps, title) {
+ const phaseNames = [
+ "New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous",
+ "Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent",
+ ];
+
+ const phase = SunCalc.getMoonIllumination(new Date());
+ const pageData = {
+ Phase: phaseNames[phase.phase],
+ };
+
+ drawData(title, pageData, null, 35);
+ drawMoon(phase.phase, g.getWidth() / 2, g.getHeight() / 2);
+
+ let m = setWatch(() => {
+ let m = moonIndexPageMenu(gps);
+ }, BTN3, {repease: false, edge: "falling"});
+}
+
+
+function drawMoonTimesPage(gps, title) {
+ const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon);
+
+ const pageData = {
+ Rise: dateToTimeString(times.rise),
+ Set: dateToTimeString(times.set),
+ };
+
+ drawData(title, pageData, null, 105);
+ drawPoints();
+
+ // Draw the moon rise position
+ const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
+ const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI);
+ drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1});
+
+ // Draw the moon set position
+ const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
+ const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
+ drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1});
+
+ let m = setWatch(() => {
+ let m = moonIndexPageMenu(gps);
+ }, BTN3, {repease: false, edge: "falling"});
+}
+
+function drawSunShowPage(gps, key, date) {
+ const pos = SunCalc.getPosition(date, gps.lat, gps.lon);
+
+ const hrs = ("0" + date.getHours()).substr(-2);
+ const mins = ("0" + date.getMinutes()).substr(-2);
+ const secs = ("0" + date.getMinutes()).substr(-2);
+ const time = `${hrs}:${mins}:${secs}`;
+
+ const azimuth = Number(pos.azimuth.toFixed(2));
+ const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
+ const altitude = Number(pos.altitude.toFixed(2));
+
+ const pageData = {
+ Time: time,
+ Altitude: altitude,
+ Azimumth: azimuth,
+ Degrees: azimuthDegrees
+ };
+
+ drawData(key, pageData, null, 85);
+
+ drawPoints();
+
+ // Draw the suns position
+ drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0});
+
+ m = setWatch(() => {
+ m = sunIndexPageMenu(gps);
+ }, BTN3, {repeat: false, edge: "falling"});
+
+ return null;
+}
+
+function sunIndexPageMenu(gps) {
+ const sunTimes = SunCalc.getTimes(new Date(), gps.lat, gps.lon);
+
+ const sunMenu = {
+ "": {
+ "title": "-- Sun --",
+ },
+ "Current Pos": () => {
+ m = E.showMenu();
+ drawSunShowPage(gps, "Current Pos", new Date());
+ },
+ };
+
+ Object.keys(sunTimes).sort().reduce((menu, key) => {
+ const title = titlizeKey(key);
+ menu[title] = () => {
+ m = E.showMenu();
+ drawSunShowPage(gps, key, sunTimes[key]);
+ };
+ return menu;
+ }, sunMenu);
+
+ sunMenu["< Back"] = () => m = indexPageMenu(gps);
+
+ return E.showMenu(sunMenu);
+}
+
+
+function moonIndexPageMenu(gps) {
+ const moonMenu = {
+ "": {
+ "title": "-- Moon --",
+ },
+ "Times": () => {
+ m = E.showMenu();
+ drawMoonTimesPage(gps, "Times");
+ },
+ "Position": () => {
+ m = E.showMenu();
+ drawMoonPositionPage(gps, "Position");
+ },
+ "Illumination": () => {
+ m = E.showMenu();
+ drawMoonIlluminationPage(gps, "Illumination");
+ },
+ "< Back": () => m = indexPageMenu(gps),
+ };
+
+ return E.showMenu(moonMenu);
+}
+
+function indexPageMenu(gps) {
+ const menu = {
+ "": {
+ "title": "Select",
+ },
+ "Sun": () => {
+ m = sunIndexPageMenu(gps);
+ },
+ "Moon": () => {
+ m = moonIndexPageMenu(gps);
+ },
+ "< Exit": () => { load(); }
+ };
+
+ return E.showMenu(menu);
+}
+
+/**
+ * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page
+ */
+function drawGPSWaitPage() {
+ const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA=="))
+
+ g.clear();
+ g.drawImage(img, 100, 50);
+ g.setFont("6x8", 1);
+ g.drawString("Astrocalc v0.01", 80, 105);
+ g.drawString("Locating GPS", 85, 140);
+ g.drawString("Please wait...", 80, 155);
+ g.flip();
+
+ const DEBUG = false;
+ if (DEBUG) {
+ const gps = {
+ "lat": 56.45783133333,
+ "lon": -3.02188583333,
+ "alt": 75.3,
+ "speed": 0.070376,
+ "course": NaN,
+ "time":new Date(),
+ "satellites": 4,
+ "fix": 1
+ };
+
+ m = indexPageMenu(gps);
+
+ return;
+ }
+
+ Bangle.on('GPS', (gps) => {
+ if (gps.fix === 0) return;
+
+ Bangle.setGPSPower(0);
+ Bangle.buzz();
+ Bangle.setLCDPower(true);
+
+ m = indexPageMenu(gps);
+ });
+}
+
+function init() {
+ Bangle.setGPSPower(1);
+ drawGPSWaitPage();
+}
+
+let m;
+init();
\ No newline at end of file
diff --git a/apps/astrocalc/astrocalc-icon.js b/apps/astrocalc/astrocalc-icon.js
new file mode 100644
index 000000000..aa04c2805
--- /dev/null
+++ b/apps/astrocalc/astrocalc-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA=="))
\ No newline at end of file
diff --git a/apps/astrocalc/astrocalc.png b/apps/astrocalc/astrocalc.png
new file mode 100644
index 000000000..c26a651ec
Binary files /dev/null and b/apps/astrocalc/astrocalc.png differ
diff --git a/apps/astrocalc/first-quarter-icon.js b/apps/astrocalc/first-quarter-icon.js
new file mode 100644
index 000000000..d88ec79b5
--- /dev/null
+++ b/apps/astrocalc/first-quarter-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("rlcgI1ygf4BZM/BZMD//wCxP/8AWJ/+ACxP+CxQ6ICwP/4AWJERAWCEQ4WCERAWCEQ4WDOg4WCNA4WD/gWKRYwWDHI4WDHIwWDHI4WDHIwWEOYwWDHIwWEKAwWD/4WKKAwWEKAoWEYgwWPM4wWEM4oWQM4oWEPwwWbPwoWESowW/C34WOZ1vACxP8Cyv4CxWACyoKFCwiUFCwhmGCwh9FCwhmGCwhmFCwhPGCwgKFCwg4GCwZPGCwg4GCwY4GCwgKGCwY4GCwZxGCwjBFCwghHCwQhHCwYhHCwQhHCwRlHCwSHHCwYKICwI3HCwQKJAFAA=="))
\ No newline at end of file
diff --git a/apps/astrocalc/full-icon.js b/apps/astrocalc/full-icon.js
new file mode 100644
index 000000000..8bc04f7fc
--- /dev/null
+++ b/apps/astrocalc/full-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("rlcgJC/AD8B//4BRILJBQP/+AKGn4LC4AKFh4KC/4KFgYKD/gLFv4LD8AKEj4KD/+AEJAiGEIgiFIYhFFOAQADOghlDNA0HBQv+Q4wADRYZaFLgg4GHIg4GHIY4GHIhxFOYhxGOYgKHKARPHKARPHKAZPHKATBFYgoWKMw5nDMw5nCCyx9IPwQKIPwIW/C34WJZ1sDBQ/8CwM/BY/ACxkfBY+AgEBBQ/4CwJ+IBQJ+IPoJnIMwRnIMwJQIJ4RQIJ4JQIJ4RQIBQQ5HHAQ5HHAY5HHARzHOIRzHOIbEHYIIACLgpaDEQwhFEQohEIopDENAplERYwKGOgZwEBYoKIAH4AXA=="))
\ No newline at end of file
diff --git a/apps/astrocalc/last-quarter-icon.js b/apps/astrocalc/last-quarter-icon.js
new file mode 100644
index 000000000..b6517f66b
--- /dev/null
+++ b/apps/astrocalc/last-quarter-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("rlcgI0xgP8BRP/4ALI/4WJv4WJj4WJg//CxA3BCxM/CxIhCCw4hCCxAhCCw4hCCxAKCCw5lBCxEDCxSHBCxA4DCw4KCCw44DCww4DCw5xCCw44DCw5PDCw0PCxQKDCwxPDCwzBDCyRmECwxmDCyRmDCwx9ECzoKDCwyUEC34W/CyDOtn4WJgYWVgIWKj4WVPwgWFSogWGM4gWGPwYWGM4gWGM4YWGKAgWGKAYWGHIgWGKAYWHHIYWGHIYWHHIYWGHIYWHOYYWHYgQWHEQYWHEQQWIEQQWHEQQWINAQWIRYIWIOgQWIHQIWJBYIWJAFI="))
\ No newline at end of file
diff --git a/apps/astrocalc/new-icon.js b/apps/astrocalc/new-icon.js
new file mode 100644
index 000000000..5d610fbe1
--- /dev/null
+++ b/apps/astrocalc/new-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("rlcgIGDh///4RHBQQLHg4KC/wKFgIKC//4BYt/BYfgBQkfBQf/wAsHFw4HCBwXwBQc/AwYLB4AhEIARIBEQn//gECgYiEIYJ2FIoQQBE4YzBDgd/NoguBNAUPKoo/BB4YhEEQIdCAYYiECQMHUwwHDEIweBLgMPWIwiBAQSlENwQTBDIQAFFQMDHAw5BOYN/HAwfB8ANCAAofCHA45B+EPHA4UBKQQAGMgMfUYQAFv+DJ45QCn5PHKAPDJ45QB/hmICwPnT4yhC/1/Mw5nBCxZmIM4P/PpB+BC34WEVZCsB/7CIYYIWWOX4WbfiwWL/gKHgf+n/ABY8/4YWJ/k/VhF/4LDIg/4j5nI/+APxEP+EPM48BCgN/KA5CBg5QHMwINCJ4/AgY5Hh4fBj45GHAKeBAQSfFMgIZCHAoqCv45GA4QOBEQsfDwQDDEIgSC/4iFv6dCg4iFj60Dn4iEEIKRCL4K5E/5uDh4QDDgKFEv4uDj4/EE4IRCDYIzEAwIvBAQKnFEQIADMIhFBAAayFNAIACMoZtDBYa9GFwbrHBQR2EBYoKEA=="))
\ No newline at end of file
diff --git a/apps/astrocalc/suncalc.js b/apps/astrocalc/suncalc.js
new file mode 100644
index 000000000..6ef5aa2d0
--- /dev/null
+++ b/apps/astrocalc/suncalc.js
@@ -0,0 +1,328 @@
+/*
+ (c) 2011-2015, Vladimir Agafonkin
+ SunCalc is a JavaScript library for calculating sun/moon position and light phases.
+ https://github.com/mourner/suncalc
+*/
+
+(function () { 'use strict';
+
+// shortcuts for easier to read formulas
+
+var PI = Math.PI,
+ sin = Math.sin,
+ cos = Math.cos,
+ tan = Math.tan,
+ asin = Math.asin,
+ atan = Math.atan2,
+ acos = Math.acos,
+ rad = PI / 180;
+
+// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
+
+
+// date/time constants and conversions
+
+var dayMs = 1000 * 60 * 60 * 24,
+ J1970 = 2440588,
+ J2000 = 2451545;
+
+function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
+function fromJulian(j) { return (j + 0.5 - J1970) * dayMs; }
+function toDays(date) { return toJulian(date) - J2000; }
+
+
+// general calculations for position
+
+var e = rad * 23.4397; // obliquity of the Earth
+
+function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
+function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
+
+function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
+function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
+
+function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
+
+function astroRefraction(h) {
+ if (h < 0) // the following formula works for positive altitudes only.
+ h = 0; // if h = -0.08901179 a div/0 would occur.
+
+ // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+ // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
+ return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
+}
+
+// general sun calculations
+
+function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
+
+function eclipticLongitude(M) {
+
+ var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
+ P = rad * 102.9372; // perihelion of the Earth
+
+ return M + C + P + PI;
+}
+
+function sunCoords(d) {
+
+ var M = solarMeanAnomaly(d),
+ L = eclipticLongitude(M);
+
+ return {
+ dec: declination(L, 0),
+ ra: rightAscension(L, 0)
+ };
+}
+
+
+var SunCalc = {};
+
+
+// calculates sun position for a given date and latitude/longitude
+
+SunCalc.getPosition = function (date, lat, lng) {
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+ d = toDays(date),
+
+ c = sunCoords(d),
+ H = siderealTime(d, lw) - c.ra;
+
+ return {
+ azimuth: azimuth(H, phi, c.dec),
+ altitude: altitude(H, phi, c.dec)
+ };
+};
+
+
+// sun times configuration (angle, morning name, evening name)
+
+var times = SunCalc.times = [
+ [-0.833, 'sunrise', 'sunset' ],
+ [ -0.3, 'sunriseEnd', 'sunsetStart' ],
+ [ -6, 'dawn', 'dusk' ],
+ [ -12, 'nauticalDawn', 'nauticalDusk'],
+ [ -18, 'nightEnd', 'night' ],
+ [ 6, 'goldenHourEnd', 'goldenHour' ]
+];
+
+// adds a custom time to the times config
+
+SunCalc.addTime = function (angle, riseName, setName) {
+ times.push([angle, riseName, setName]);
+};
+
+
+// calculations for sun times
+
+var J0 = 0.0009;
+
+function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
+
+function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
+function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
+
+function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
+function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }
+
+// returns set time for the given sun altitude
+function getSetJ(h, lw, phi, dec, n, M, L) {
+
+ var w = hourAngle(h, phi, dec),
+ a = approxTransit(w, lw, n);
+ return solarTransitJ(a, M, L);
+}
+
+
+// calculates sun times for a given date, latitude/longitude, and, optionally,
+// the observer height (in meters) relative to the horizon
+
+SunCalc.getTimes = function (date, lat, lng, height) {
+
+ height = height || 0;
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+
+ dh = observerAngle(height),
+
+ d = toDays(date),
+ n = julianCycle(d, lw),
+ ds = approxTransit(0, lw, n),
+
+ M = solarMeanAnomaly(ds),
+ L = eclipticLongitude(M),
+ dec = declination(L, 0),
+
+ Jnoon = solarTransitJ(ds, M, L),
+
+ i, len, time, h0, Jset, Jrise;
+
+
+ var result = {
+ solarNoon: new Date(fromJulian(Jnoon)),
+ nadir: new Date(fromJulian(Jnoon - 0.5))
+ };
+
+ for (i = 0, len = times.length; i < len; i += 1) {
+ time = times[i];
+ h0 = (time[0] + dh) * rad;
+
+ Jset = getSetJ(h0, lw, phi, dec, n, M, L);
+ Jrise = Jnoon - (Jset - Jnoon);
+
+ result[time[1]] = new Date(fromJulian(Jrise) - (dayMs / 2));
+ result[time[2]] = new Date(fromJulian(Jset) + (dayMs / 2));
+ }
+
+ return result;
+};
+
+
+// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
+
+function moonCoords(d) { // geocentric ecliptic coordinates of the moon
+
+ var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
+ M = rad * (134.963 + 13.064993 * d), // mean anomaly
+ F = rad * (93.272 + 13.229350 * d), // mean distance
+
+ l = L + rad * 6.289 * sin(M), // longitude
+ b = rad * 5.128 * sin(F), // latitude
+ dt = 385001 - 20905 * cos(M); // distance to the moon in km
+
+ return {
+ ra: rightAscension(l, b),
+ dec: declination(l, b),
+ dist: dt
+ };
+}
+
+SunCalc.getMoonPosition = function (date, lat, lng) {
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+ d = toDays(date),
+
+ c = moonCoords(d),
+ H = siderealTime(d, lw) - c.ra,
+ h = altitude(H, phi, c.dec),
+ // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+ pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
+
+ h = h + astroRefraction(h); // altitude correction for refraction
+
+ return {
+ azimuth: azimuth(H, phi, c.dec),
+ altitude: h,
+ distance: c.dist,
+ parallacticAngle: pa
+ };
+};
+
+
+// calculations for illumination parameters of the moon,
+// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
+// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+
+// Function updated from gist: https://gist.github.com/endel/dfe6bb2fbe679781948c
+
+SunCalc.getMoonIllumination = function (date) {
+ let month = date.getMonth();
+ let year = date.getFullYear();
+ let day = date.getDate();
+
+ let c = 0;
+ let e = 0;
+ let jd = 0;
+ let b = 0;
+
+ if (month < 3) {
+ year--;
+ month += 12;
+ }
+
+ ++month;
+ c = 365.25 * year;
+ e = 30.6 * month;
+ jd = c + e + day - 694039.09; // jd is total days elapsed
+ jd /= 29.5305882; // divide by the moon cycle
+ b = parseInt(jd); // int(jd) -> b, take integer part of jd
+ jd -= b; // subtract integer part to leave fractional part of original jd
+ b = Math.round(jd * 8); // scale fraction from 0-8 and round
+
+ if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0
+
+ return {phase: b};
+};
+
+
+function hoursLater(date, h) {
+ return new Date(date.valueOf() + h * dayMs / 24);
+}
+
+// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
+
+SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
+ var t = date;
+ if (inUTC) t.setUTCHours(0, 0, 0, 0);
+ else t.setHours(0, 0, 0, 0);
+
+ var hc = 0.133 * rad,
+ h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
+ h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
+
+ // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
+ for (var i = 1; i <= 24; i += 2) {
+ h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
+ h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
+
+ a = (h0 + h2) / 2 - h1;
+ b = (h2 - h0) / 2;
+ xe = -b / (2 * a);
+ ye = (a * xe + b) * xe + h1;
+ d = b * b - 4 * a * h1;
+ roots = 0;
+
+ if (d >= 0) {
+ dx = Math.sqrt(d) / (Math.abs(a) * 2);
+ x1 = xe - dx;
+ x2 = xe + dx;
+ if (Math.abs(x1) <= 1) roots++;
+ if (Math.abs(x2) <= 1) roots++;
+ if (x1 < -1) x1 = x2;
+ }
+
+ if (roots === 1) {
+ if (h0 < 0) rise = i + x1;
+ else set = i + x1;
+
+ } else if (roots === 2) {
+ rise = i + (ye < 0 ? x2 : x1);
+ set = i + (ye < 0 ? x1 : x2);
+ }
+
+ if (rise && set) break;
+
+ h0 = h2;
+ }
+
+ var result = {};
+
+ if (rise) result.rise = hoursLater(t, rise);
+ if (set) result.set = hoursLater(t, set);
+
+ if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
+
+ return result;
+};
+
+
+// export as Node module / AMD module / browser variable
+if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc;
+else if (typeof define === 'function' && define.amd) define(SunCalc);
+else global.SunCalc = SunCalc;
+
+}());
diff --git a/apps/astrocalc/waning-crescent-icon.js b/apps/astrocalc/waning-crescent-icon.js
new file mode 100644
index 000000000..8ff83ab1f
--- /dev/null
+++ b/apps/astrocalc/waning-crescent-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("rlcgJC/ABHgBRN8BRMfwAKIg/4CxP/BRM/HBMH/wKIgP/4AhJ/ghJ/5PJ/5PJj4WJgf/+AWIv5mJHAIWJ/5mJHAJ9IHAIWJn59JHAJ9JJ4IWIh4WK/4WJJ4KUIYIKUJJ4IWIMwIWgMwIWIPoLCJCwLCICxYKBCxCUBC34W/Cya3WCxr8In78JgYWhj4WJgIWKPwP8SpXAM5IWJPwIWIKAIWJM4PgKBP+CxBQBCxA5CBRBQBYZA5CBRA5BSpA5CSpA5BCxJzBPxDEBPxIiBM5MDPxJFBM5IiBKBMBKBKLBKBMAhwKJAH4ABA="))
\ No newline at end of file
diff --git a/apps/astrocalc/waning-gibbous-icon.js b/apps/astrocalc/waning-gibbous-icon.js
new file mode 100644
index 000000000..2373475f4
--- /dev/null
+++ b/apps/astrocalc/waning-gibbous-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("rlcgI0xgP/wAKJ/wWI///+AKHv4LBEQ8fBQP8BQ0HBQP/8A3HAAQWGn4KCHIwhDHIwhE/AhJ//AEJJQGBQZQGMoQABRQsDCwhQFQ4RnHHAgWGBQhnFHAhnFHAoWFOIhnFHAp+FJ4oWEh4WKBQp+EJ4qVEYIgWRMwwWEMwoWLVghmFVgh9GCzYKGCwaUGC34W/CxzOtn4WJgYKF/wWK8AKCgIWKj4WVPwwWDSo38BQZnG4B+JCwhnGCwhnF/AKDKA2AKBIWEHIwKEKAqrDHI4KEHIp9EHIqUEHIxmEOYp9EYgxmEEQpmFEQoKFEQhmFEQhPGNAhPFRYg4GOggKHHQSIFBYghIAFQ="))
\ No newline at end of file
diff --git a/apps/astrocalc/waxing-crescent-icon.js b/apps/astrocalc/waxing-crescent-icon.js
new file mode 100644
index 000000000..d89525c88
--- /dev/null
+++ b/apps/astrocalc/waxing-crescent-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("rlcgJC/ABdwBRMD8ALJj+ABREB/wWJh/wBZN/4AKIg4iKn/4KBP/ERMfERMB/5FJj//NBP//hnJ/6LJ/45Jg45Kv45JCwI5Jn5zJPwI5JCwJQICwP/CxRQISoJQJSoLEICwRQICwJnICzJnIYYJ+JCzB+ICwKVJC34W/CxbOffgIWIfgXACxP8Cyv4CxWACyUDPpU/ShIWBPpIWBPpEHMxMAv5mJCwJPICwQKIYQI4IYQJPJCwI4ISgI4JSgIKICwI4Jn5xJSgLBIMwIhJg4hJMwIKJj4hJgJlJgE+BRMHBRIA+A"))
\ No newline at end of file
diff --git a/apps/astrocalc/waxing-gibbous-icon.js b/apps/astrocalc/waxing-gibbous-icon.js
new file mode 100644
index 000000000..90ccd6f37
--- /dev/null
+++ b/apps/astrocalc/waxing-gibbous-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("rlcgI1yj/4BREH/4LJ/4LJj4LB8AKGgYKB/+ABY1/BQP+BQ0PCwQuHBQX/4A4IEQ8BCwYiGn4iJJ4YiHJ4QAB+CIGAAZoFBQn8MxCLHBQg5FMwY5GMwg5GCwo5EMwhzGPog5FCwxQECwv/PpJQFSghQFCwzEECyJnECwxnDVYoWFBQpnECwx+ECzp+DCwyVEC34W/CyDOt4AKCg4KF/gWDv4WQ/AWKwAWVBQcDShMAn5mJCwx9DCwxmEgJmJgEfJ5IWGBQasGHAisFJ4gWGHAh+FHAiVGBQhnFHAp+EOIhnGYIZnGEIpQEEIxnEEIpQEEIxQDMoo5EQ4o5FFgyKDBRAiBBRAApA="))
\ No newline at end of file
diff --git a/apps/astroid/asteroids.json b/apps/astroid/asteroids.json
deleted file mode 100644
index 7215be590..000000000
--- a/apps/astroid/asteroids.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Asteroids!","type":"app",
- "icon":"*astroid",
- "src":"-astroid"
-}
diff --git a/apps/balltastic/ChangeLog b/apps/balltastic/ChangeLog
new file mode 100644
index 000000000..5a62086c2
--- /dev/null
+++ b/apps/balltastic/ChangeLog
@@ -0,0 +1 @@
+0.01: Initial version of Balltastic released! Happy!
diff --git a/apps/balltastic/app-icon.js b/apps/balltastic/app-icon.js
new file mode 100644
index 000000000..f25b6e067
--- /dev/null
+++ b/apps/balltastic/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEogAIkUzmciBpIVIkYWBAAUyCx0hiIXFAAMkCxhUBC4fzDAYWLiAXFAAP//5KKoMRC4UTC4k/DAPzJJERiKcCC5H/GA4uBWwp6DC4YwHCwMBDI0SMAYwHoIWBiXxdIwYCMJCjBiM/C46VDC4M/GAkRgMf//ySAQAEgKrDC4lBgMCHIQXHSwxICIwIuBAAIXIGAYABiQXBkEBTYcgC473FkQXBiETTQZ4IgECC4cholCiJGDMAIXIWgIXCmMkC4JGDJBbEDC4UACwn/mAtGSYsxilCgIXFSAqDBkMRiIFBkcxiUiC4sxXowIBC4QGBkIXBiJ2EFwsDBIPyC4ILBgMRiUyiCmJgSCC+YXDgAXDR4YuEcAn/MAIXEmcgBoXyFwjIEMAQXFkIOCUgoXF+J3CC4cxBwR1IQQx3BkUzmUSBQKkFC5IuBkVDJAJeGRwLhHFwUkC4Mxl6lFC48gFwYXCmcTOwomBC4swYIMikU0C4UxkJ3FC40xFoIXCogXBmaxDC5MyCwUiogXDmIXTJASSBC4kRU4oXDkgXFmQwDNwIWEBoIXFJAYKBZggWFC4YWCC4g7BkIWBkYWBBYYXCkYXDJAYjDkQUEEYZGEGA4XIIwwwGDAQuOGAomCFo4uGGARoBE4ZOGFxAABBwgAICxAABCyxJBGJJFJJRgVNPggsMA="))
diff --git a/apps/balltastic/app.js b/apps/balltastic/app.js
new file mode 100644
index 000000000..6c1de940c
--- /dev/null
+++ b/apps/balltastic/app.js
@@ -0,0 +1,186 @@
+Bangle.setLCDBrightness(1);
+Bangle.setLCDMode("doublebuffered");
+
+let points = 0;
+let level = 1;
+let levelSpeedStart = 0.8;
+let nextLevelPoints = 20;
+let levelSpeedFactor = 0.2;
+let counterWidth = 10;
+let gWidth = g.getWidth() - counterWidth;
+let gHeight = g.getHeight();
+let counter = 160;
+let counterMax = 160;
+let ballDims = 20;
+let ballx = g.getWidth() / 2 - ballDims;
+let bally = g.getHeight() / 2 - ballDims;
+let dotx = g.getWidth() / 2;
+let doty = g.getWidth() / 2;
+let ballBuzzTime = 5;
+let ballSpeedFactor = 40;
+let redrawspeed = 5;
+let dotwidth = 5;
+let running = false;
+let drawInterval;
+let xBuzzed = false;
+let yBuzzed = false;
+
+let BALL = require("heatshrink").decompress(
+ atob(
+ "ikUyAROvkQ3v4405AIYHBGq9KpMhktz1/W7feAJAtBEZ9jhkhs0ZgkQ8lKxW+jAdB516627E4X8AIPWzelmolKlpJBjMFEYIpC4kQ0YBBqWKynTFYPe7gpE3ec6gnHkNFrXL7372u2E4WjhGCAIliqWrUIPeKoIpB7h9HoUoqWq999///FIJ3BhGDEIIBBgFBAoWCoUI3vY62aQIW7ymSJooLBEoIADwkQEYVhEoInEGIOjR4O1y/OrIrBUYdr198iH/74nF88cE4gpCA4MY8k59CzBAINrx2164nBtduufPWYIlF++/xkxNoMAAIJPBoSdB52a30ZkNGE4IvBoUpwkxLIOMyWEmAmE7+MqKbEsLLBH4P3zw1BAYJFBFIMY8sQ4cx44nB0tVHYITBEoO967lDgDDC1tVQ4QBD37xBjMmJ4I3BE4IxBPoOMuSrBHYL1BJYbrDvfPLoYBD889jMlEoMhkpJBwkRE4O+jB7B405LoJPEYYUx0xPG7/3vxvBmOnrXsdIOc6jxBE4JfBvfwHIafDFoMRgh3H99+zsUDIOMqWU2YlBAAO1/AnBToN76EhgpTBFYKPBGIIhBEovOrWliuc2YlBE4oABE4etu2UyVrpqJBMoKvBEIPnjvWze97ATBE4YPBEopRC64BC27nBzn0znTAIOlimtq21y4BCEoM1HYOMqIVBE44AB0tVCYIBEigVBE4U1GYIFBymywkwEoJzHABIRBMIIXBWoIDCqOEmOEiABCmIjPAA51BFoVSEoUwAIIZNA"
+ )
+);
+
+function reset() {
+ g.clear();
+ level = 1;
+ points = 0;
+ ballx = g.getWidth() / 2 - ballDims;
+ bally = g.getHeight() / 2 - ballDims;
+ counter = counterMax;
+ createRandomDot();
+ drawInterval = setInterval(play, redrawspeed);
+ running = true;
+}
+
+function collide() {
+ try {
+ Bangle.buzz(ballBuzzTime, 0.8);
+ } catch (e) {}
+}
+
+function createRandomDot() {
+ dotx = Math.floor(
+ Math.random() * Math.floor(gWidth - dotwidth / 2) + dotwidth / 2
+ );
+ doty = Math.floor(
+ Math.random() * Math.floor(gHeight - dotwidth / 2) + dotwidth / 2
+ );
+}
+
+function checkIfDotEaten() {
+ if (
+ ballx + ballDims > dotx &&
+ ballx <= dotx + dotwidth &&
+ bally + ballDims > doty &&
+ bally <= doty + dotwidth
+ ) {
+ collide();
+ createRandomDot();
+ counter = counterMax;
+ points++;
+
+ if (points % nextLevelPoints == 0) {
+ level++;
+ }
+ }
+}
+
+function drawLevelText() {
+ g.setColor("#26b6c7");
+ g.setFontAlign(0, 0);
+ g.setFont("4x6", 5);
+ g.drawString("Level " + level, 120, 80);
+}
+
+function draw() {
+ //bg
+ g.setColor("#71c6cf");
+ g.fillRect(0, 0, g.getWidth(), g.getHeight());
+
+ //counter
+ drawCounter();
+
+ //draw level
+ drawLevelText();
+
+ //dot
+ g.setColor("#ff0000");
+ g.fillCircle(dotx, doty, dotwidth);
+
+ //ball
+ g.drawImage(BALL, ballx, bally);
+
+ g.flip();
+}
+
+function drawCounter() {
+ g.setColor("#000000");
+ g.fillRect(g.getWidth() - counterWidth, 0, g.getWidth(), gHeight);
+
+ if(counter < 40 ) g.setColor("#fc0303");
+ else if (counter < 80 ) g.setColor("#fc9803");
+ else g.setColor("#0318fc");
+
+ g.fillRect(
+ g.getWidth() - counterWidth,
+ gHeight,
+ g.getWidth(),
+ gHeight - counter
+ );
+}
+
+function checkCollision() {
+ if (ballx < 0) {
+ ballx = 0;
+ if (!xBuzzed) collide();
+ xBuzzed = true;
+ } else if (ballx > gWidth - ballDims) {
+ ballx = gWidth - ballDims;
+ if (!xBuzzed) collide();
+ xBuzzed = true;
+ } else {
+ xBuzzed = false;
+ }
+
+ if (bally < 0) {
+ bally = 0;
+ if (!yBuzzed) collide();
+ yBuzzed = true;
+ } else if (bally > gHeight - ballDims) {
+ bally = gHeight - ballDims;
+ if (!yBuzzed) collide();
+ yBuzzed = true;
+ } else {
+ yBuzzed = false;
+ }
+}
+
+function count() {
+ counter -= levelSpeedStart + level * levelSpeedFactor;
+ if (counter <= 0) {
+ running = false;
+ clearInterval(drawInterval);
+ setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Gameover!");},50);
+ }
+}
+
+function accel(values) {
+ ballx -= values.x * ballSpeedFactor;
+ bally -= values.y * ballSpeedFactor;
+}
+
+function play() {
+ if (running) {
+ accel(Bangle.getAccel());
+ checkCollision();
+ checkIfDotEaten();
+ count();
+ draw();
+ }
+}
+
+setTimeout(() => {
+ reset();
+ drawInterval = setInterval(play, redrawspeed);
+
+ setWatch(
+ () => {
+ if(!running) reset();
+ },
+ BTN1,
+ { repeat: true }
+ );
+
+ running = true;
+}, 10);
diff --git a/apps/balltastic/app.png b/apps/balltastic/app.png
new file mode 100644
index 000000000..0f95e056f
Binary files /dev/null and b/apps/balltastic/app.png differ
diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog
new file mode 100644
index 000000000..2e0fd088c
--- /dev/null
+++ b/apps/barclock/ChangeLog
@@ -0,0 +1,4 @@
+0.01: Created Bar Clock
+0.02: Apply locale, 12-hour setting
+0.03: Fix dates drawing over each other at midnight
+0.04: Small bugfix
diff --git a/apps/barclock/clock-bar-icon.js b/apps/barclock/clock-bar-icon.js
new file mode 100644
index 000000000..29bf0f481
--- /dev/null
+++ b/apps/barclock/clock-bar-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgJC/AD8Mgfwh/AhgFFngHBOIM8AovMDIXA5gFFDoUAmYjDAocMSoMz/4FF//P/g1CAopTLDAIABwAFGAH4AfA"))
diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js
new file mode 100644
index 000000000..da436daee
--- /dev/null
+++ b/apps/barclock/clock-bar.js
@@ -0,0 +1,166 @@
+/* jshint esversion: 6 */
+/**
+ * A simple digital clock showing seconds as a bar
+ **/
+{
+ // Check settings for what type our clock should be
+ const is12Hour = (require('Storage').readJSON('setting.json', 1) || {})['12hour']
+ let locale = require('locale')
+ { // add some more info to locale
+ let date = new Date()
+ date.setFullYear(1111)
+ date.setMonth(1, 3) // februari: months are zero-indexed
+ const localized = locale.date(date, true)
+ locale.dayFirst = /3.*2/.test(localized)
+ locale.hasMeridian = (locale.meridian(date) !== '')
+ }
+ const screen = {
+ width: g.getWidth(),
+ height: g.getWidth(),
+ middle: g.getWidth() / 2,
+ center: g.getHeight() / 2,
+ }
+
+ // hardcoded "settings"
+ const settings = {
+ time: {
+ color: -1,
+ font: '6x8',
+ size: (is12Hour && locale.hasMeridian) ? 6 : 8,
+ middle: screen.middle,
+ center: screen.center,
+ ampm: {
+ color: -1,
+ font: '6x8',
+ size: 2,
+ },
+ },
+ date: {
+ color: -1,
+ font: 'Vector',
+ size: 20,
+ middle: screen.height - 20, // at bottom of screen
+ center: screen.center,
+ },
+ bar: {
+ color: -1,
+ top: 155, // just below time
+ thickness: 6, // matches 24h time "pixel" size
+ },
+ }
+
+ const SECONDS_PER_MINUTE = 60
+
+ const timeText = function (date) {
+ if (!is12Hour) {
+ return locale.time(date, true)
+ }
+ const date12 = new Date(date.getTime())
+ const hours = date12.getHours()
+ if (hours === 0) {
+ date12.setHours(12)
+ } else if (hours > 12) {
+ date12.setHours(hours - 12)
+ }
+ return locale.time(date12, true)
+ }
+ const ampmText = function (date) {
+ return is12Hour ? locale.meridian(date) : ''
+ }
+
+ const dateText = function (date) {
+ const dayName = locale.dow(date, true),
+ month = locale.month(date, true),
+ day = date.getDate()
+ const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`
+ return `${dayName} ${dayMonth}`
+ }
+
+ const drawDateTime = function (date) {
+ const t = settings.time
+ g.setColor(t.color)
+ g.setFont(t.font, t.size)
+ g.setFontAlign(0, 0) // centered
+ g.drawString(timeText(date), t.center, t.middle, true)
+ if (is12Hour && locale.hasMeridian) {
+ const a = settings.time.ampm
+ g.setColor(a.color)
+ g.setFont(a.font, a.size)
+ g.setFontAlign(1, -1) // right top
+ // at right edge of screen, aligned with time bottom
+ const left = screen.width - a.size * 2,
+ top = t.middle + t.size - a.size
+ g.drawString(ampmText(date), left, top, true)
+ }
+
+ const d = settings.date
+ g.setColor(d.color)
+ g.setFont(d.font, d.size)
+ g.setFontAlign(0, 0) // centered
+ g.drawString(dateText(date), d.center, d.middle, true)
+ }
+
+ const drawBar = function (date) {
+ const b = settings.bar
+ const seconds = date.getSeconds()
+ if (seconds === 0) {
+ // zero-size rect stills draws one line of pixels, we don't want that
+ return
+ }
+ const fraction = seconds / SECONDS_PER_MINUTE,
+ width = fraction * screen.width
+ g.setColor(b.color)
+ g.fillRect(0, b.top, width, b.top + b.thickness)
+ }
+
+ const clearScreen = function () {
+ g.setColor(0)
+ const timeTop = settings.time.middle - (settings.time.size * 4)
+ g.fillRect(0, timeTop, screen.width, screen.height)
+ }
+
+ let lastSeconds
+ const tick = function () {
+ g.reset()
+ const date = new Date()
+ const seconds = date.getSeconds()
+ if (lastSeconds > seconds) {
+ // new minute
+ clearScreen()
+ drawDateTime(date)
+ }
+ // the bar only gets larger, so drawing on top of the previous one is fine
+ drawBar(date)
+
+ lastSeconds = seconds
+ }
+
+ let iTick
+ const start = function () {
+ lastSeconds = 99 // force redraw
+ tick()
+ iTick = setInterval(tick, 1000)
+ }
+ const stop = function () {
+ if (iTick) {
+ clearInterval(iTick)
+ iTick = undefined
+ }
+ }
+
+ // clean app screen
+ g.clear()
+ Bangle.loadWidgets()
+ Bangle.drawWidgets()
+ // Show launcher when middle button pressed
+ setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'})
+
+ Bangle.on('lcdPower', function (on) {
+ if (on) {
+ start()
+ } else {
+ stop()
+ }
+ })
+ start()
+}
diff --git a/apps/barclock/clock-bar.png b/apps/barclock/clock-bar.png
new file mode 100644
index 000000000..a580cae69
Binary files /dev/null and b/apps/barclock/clock-bar.png differ
diff --git a/apps/bclock/clock-binary.json b/apps/bclock/clock-binary.json
deleted file mode 100644
index c00dd9d76..000000000
--- a/apps/bclock/clock-binary.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name":"Binary Clock",
- "type":"clock",
- "icon":"*bclock",
- "src":"-bclock"
- }
-
\ No newline at end of file
diff --git a/apps/beer/beercompass.html b/apps/beer/beercompass.html
index ba3988aec..434f0f6a9 100644
--- a/apps/beer/beercompass.html
+++ b/apps/beer/beercompass.html
@@ -33,6 +33,7 @@
If ok, Click
+
@@ -195,21 +196,12 @@ Bangle.on('mag', function(m) {
Bangle.setCompassPower(1);
Bangle.setGPSPower(1);
g.clear();`;
- var json = JSON.stringify({
- name:"Beer Compass",
- icon:"*beer",
- src:"-beer"
-});
var icon = `require("heatshrink").decompress(atob("mEwghC/AB0O/4AG8AXNgYXHmAXl94XH+AXNn4XH/wXW+YX/C6oWHAAIXN7sz9vdAAoXN9sznvuAAXf/vuC53jC4Xd7wXQ93jn3u9vv9vt7wXT/4tBAgIXQ7wvCC4PgC5sO6czIQJfBC6PumaPDC6wwCC50NYAJcBVgIDBCxrAFbgYXP7yoDF6TADL4YXPVAIXCRyAXC7wXW9zwBC6cNC9zABC4gWQC653CR4fQC6x3TF6gXXI4M9d6wAEC9EN73dAAZfQgczAAkwC/4XXAH4"))`;
-
- window.postMessage({
- id : "beer",
-
+ sendCustomizedApp({
storage:[
- {name:"-beer", content:app},
- {name:"+beer", content:json},
- {name:"*beer", content:icon, evaluate:true},
+ {name:"beer.app.js", content:app},
+ {name:"beer.img", content:icon, evaluate:true},
]
});
});
diff --git a/apps/berlinc/berlin-clock.json b/apps/berlinc/berlin-clock.json
deleted file mode 100644
index 7c0d6e411..000000000
--- a/apps/berlinc/berlin-clock.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name":"Berlin Clock",
- "type":"clock",
- "icon":"*berlinc",
- "src":"-berlinc"
-}
diff --git a/apps/blescan/blescan.json b/apps/blescan/blescan.json
deleted file mode 100644
index d9e7f28f1..000000000
--- a/apps/blescan/blescan.json
+++ /dev/null
@@ -1,7 +0,0 @@
-
-{
- "name": "BLE Scanner",
- "type":"app",
- "icon": "*blescan",
- "src": "-blescan"
-}
diff --git a/apps/blobclk/clock-blob.json b/apps/blobclk/clock-blob.json
deleted file mode 100644
index b548acd80..000000000
--- a/apps/blobclk/clock-blob.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name":"Large Clock",
- "type":"clock",
- "icon":"*blobclk",
- "src":"-blobclk",
- "sortorder":-10
-}
diff --git a/apps/boldclk/bold_clock.json b/apps/boldclk/bold_clock.json
deleted file mode 100644
index 45ceede6b..000000000
--- a/apps/boldclk/bold_clock.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name":"Bold Clock",
- "type":"clock",
- "icon":"*boldclk",
- "src":"-boldclk",
- "sortorder":-10
-}
\ No newline at end of file
diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog
index e1bb12a32..7ab79a5a5 100644
--- a/apps/boot/ChangeLog
+++ b/apps/boot/ChangeLog
@@ -1,2 +1,15 @@
0.02: Attempt to reset state of the interpreter better before loading an app
0.03: Fix issue switching clockfaces via menu
+0.04: Add alarm functionality
+0.05: Add Welcome screen on boot
+0.06: Disable GPS time log messages, add (default=1) setting to hide log messages
+0.07: Fix issues with alarm scheduling
+0.08: Fix issues if BLE=off, 'Make Connectable' is chosen, and the loader resets Bangle.js (fix #108)
+0.09: Only check GPS for time after a fresh boot
+0.10: Stop users calling save() (fix #125)
+ If Debug info is set to 'show' don't move to Terminal if connected!
+0.11: Added vibrate as beep workaround
+0.12: Add an event on BTN2 to open launcher when no clock detected (fix #147)
+0.13: Now automatically load *.boot.js at startup
+ Move alarm code into alarm.boot.js
+0.14: Move welcome loaders to *.boot.js
diff --git a/apps/boot/boot0.js b/apps/boot/boot0.js
index 9f34f1b15..dd3b3a9ba 100644
--- a/apps/boot/boot0.js
+++ b/apps/boot/boot0.js
@@ -1,40 +1,45 @@
// This ALWAYS runs at boot
E.setFlags({pretokenise:1});
-// All of this is just shim for older Bangles
-if (!Bangle.loadWidgets) {
- Bangle.loadWidgets = function(){
- global.WIDGETPOS={tl:32,tr:g.getWidth()-32,bl:32,br:g.getWidth()-32};
- global.WIDGETS={};
- require("Storage").list().filter(a=>a[0]=='=').forEach(widget=>eval(require("Storage").read(widget)));
- };
- Bangle.drawWidgets = function(){
- for(var w of WIDGETS)w.draw()
- };
- Bangle.showLauncher = function(){
- var l = require("Storage").list().filter(a=>a[0]=='+').map(app=>{
- try { return require("Storage").readJSON(app); } catch (e) {}
- }).find(app=>app.type=="launch");
- if (l) load(l.src);
- else E.showMessage("Launcher\nnot found");
- };
- var _load = load;
- global.load = function(x) {
- if (!x) _load(x);
- else setTimeout(function(){
- // attempt to remove any currently-running code
- delete Bangle.buzz;
- delete Bangle.beep;
- Bangle.setLCDOffset&&Bangle.setLCDOffset(0);
- Bangle.setLCDMode("direct");
- g.clear();
- clearInterval();
- clearWatch();
- Bangle.removeAllListeners();
- NRF.removeAllListeners();
- Bluetooth.removeAllListeners();
- E.removeAllListeners();
- for (var i in global) if (i!="g") delete global[i];
- setTimeout('eval(require("Storage").read("'+x+'"));',20);
- },20);
+// Load settings...
+var s = require('Storage').readJSON('setting.json',1)||{};
+if (s.ble!==false) {
+ if (s.HID) { // Human interface device
+ Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQUIGQEpBZEClQF1A5EBlQZ1CBUAJXMFBxkAKXOBAAkFFQAm/wB1CJUCsQLABQwJAaEBhQEVACUBdQGVAQm1gQIJtoECCbeBAgm4gQIJzYECCeKBAgnpgQIJ6oECwA=="));
+ NRF.setServices({}, {uart:true, hid:Bangle.HID});
}
}
+if (s.blerepl===false) { // If not programmable, force terminal off Bluetooth
+ if (s.log) Terminal.setConsole(true); // if showing debug, force REPL onto terminal
+ else E.setConsole(null,{force:true}); // on new (2v05+) firmware we have E.setConsole which allows a 'null' console
+} else {
+ if (s.log && !NRF.getSecurityStatus().connected) Terminal.setConsole(); // if showing debug, put REPL on terminal (until connection)
+ else Bluetooth.setConsole(true); // else if no debug, force REPL to Bluetooth
+}
+// we just reset, so BLE should be on.
+// Don't disconnect if something is already connected to us
+if (s.ble===false && !NRF.getSecurityStatus().connected) NRF.sleep();
+// Set time, vibrate, beep, etc
+if (!s.vibrate) Bangle.buzz=Promise.resolve;
+if (s.beep===false) Bangle.beep=Promise.resolve;
+else if (s.beep=="vib") Bangle.beep = function (time, freq) {
+ return new Promise(function(resolve) {
+ if ((0|freq)<=0) freq=4000;
+ if ((0|time)<=0) time=200;
+ if (time>5000) time=5000;
+ analogWrite(D13,0.1,{freq:freq});
+ setTimeout(function() {
+ digitalWrite(D13,0);
+ resolve();
+ }, time);
+ });
+};
+Bangle.setLCDTimeout(s.timeout);
+if (!s.timeout) Bangle.setLCDPower(1);
+E.setTimeZone(s.timezone);
+delete s;
+// stop users doing bad things!
+global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }
+// Load *.boot.js files
+require('Storage').list(/\.boot\.js/).map(bootFile=>{
+ eval(require('Storage').read(bootFile));
+});
diff --git a/apps/boot/bootloader.js b/apps/boot/bootloader.js
index e73e9c955..c16984f10 100644
--- a/apps/boot/bootloader.js
+++ b/apps/boot/bootloader.js
@@ -1,22 +1,38 @@
// This runs after a 'fresh' boot
-var settings;
-try {
- settings = require("Storage").readJSON('@setting');
-} catch (e) {
- settings = {}
-}
+var settings=require("Storage").readJSON('setting.json',1)||{};
// load clock if specified
var clockApp = settings.clock;
if (clockApp) clockApp = require("Storage").read(clockApp)
if (!clockApp) {
- var clockApps = require("Storage").list().filter(a=>a[0]=='+').map(app=>{
- try { return require("Storage").readJSON(app); }
- catch (e) {}
- }).filter(app=>app.type=="clock").sort((a, b) => a.sortorder - b.sortorder);
+ var clockApps = require("Storage").list(/\.info$/).map(app=>require("Storage").readJSON(app,1)||{}).filter(app=>app.type=="clock").sort((a, b) => a.sortorder - b.sortorder);
if (clockApps && clockApps.length > 0)
clockApp = require("Storage").read(clockApps[0].src);
delete clockApps;
}
-if (clockApp) eval(clockApp);
-else E.showMessage("No Clock Found");
-delete clockApp;
+if (!clockApp) clockApp=`E.showMessage("No Clock Found");
+setWatch(() => {
+ Bangle.showLauncher();
+}, BTN2, {repeat:false,edge:"falling"});)
+`;
+delete settings;
+// check to see if our clock is wrong - if it is use GPS time
+if ((new Date()).getFullYear()==1970) {
+ E.showMessage("Searching for\nGPS time");
+ Bangle.on('GPS',function cb(g) {
+ Bangle.setGPSPower(0);
+ Bangle.removeListener("GPS",cb);
+ if (!g.time || (g.time.getFullYear()<2000) ||
+ (g.time.getFullYear()==2250)) {
+ // GPS receiver's time not set - just boot clock anyway
+ eval(clockApp);delete clockApp;
+ return;
+ }
+ // We have a GPS time. Set time and reboot (to load alarms properly)
+ setTime(g.time.getTime()/1000);
+ load();
+ });
+ Bangle.setGPSPower(1);
+} else {
+ eval(clockApp);
+ delete clockApp;
+}
diff --git a/apps/boot/bootloader.json b/apps/boot/bootloader.json
deleted file mode 100644
index dc568a319..000000000
--- a/apps/boot/bootloader.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "name":"Bootloader","type":"boot"
-}
diff --git a/apps/chrono/chrono-icon.js b/apps/chrono/chrono-icon.js
new file mode 100644
index 000000000..580713825
--- /dev/null
+++ b/apps/chrono/chrono-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwglihGIxAWUwADBDCYTDhAXSFwQEGIxowBL4QXTx///AXWF6qnBwCTDO6EIF4KnEDwLWO/4QFx7FNdwQQEGwP4GBYUB/4QBDIYXMIgQAEDIIKCVwItJFggFEx4uKCAQUBX4QDC/B2KhASCAQP/AQQcDLpQlCLgQsCCoIGBC5IkCFon/xwxCDgIXJFwYxFHIR3ILwIkBCIeIFwQHBHgReIJAgCBOoP+MYZIHhB1EDgIRBA4ZIJC4LrEMYvoAgQXJxHvI4gtDC5OIF4QSDbYY3EC5QAKG4QXNPwg0BSBAJCIQhLCDwgXKIAwXUMo4XPFwrwKC4YOCUooVCR453DIxIXJU4IqDxwXJa45FDdgxnEC40IC4TbINQYXIRQZwDAAXv/xuBCwoXBVAgXDA4wXGSARcEC4o7BRwx4DOon+C4YiCLwxIDDAobDEYJGIGAYYBxDAD9AJDC5IwCDIYACJARGIDAapDaooWLDAZhEAoIWNMggADCqAAPA"))
\ No newline at end of file
diff --git a/apps/chrono/chrono.js b/apps/chrono/chrono.js
new file mode 100644
index 000000000..cd50b8a22
--- /dev/null
+++ b/apps/chrono/chrono.js
@@ -0,0 +1,73 @@
+function msToTime(duration) {
+ var milliseconds = parseInt((duration % 1000) / 100),
+ seconds = Math.floor((duration / 1000) % 60),
+ minutes = Math.floor((duration / (1000 * 60)) % 60),
+ hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
+
+ hours = (hours < 10) ? "0" + hours : hours;
+ minutes = (minutes < 10) ? "0" + minutes : minutes;
+ seconds = (seconds < 10) ? "0" + seconds : seconds;
+
+ return hours + ":" + minutes + ":" + seconds;
+}
+
+
+var counter = 0;
+var started = false;
+
+function drawInterface() {
+ g.clear();
+ g.setFontAlign(0, 0);
+ g.setFont("6x8", 2);
+ g.drawString("+5m", g.getWidth() - 30, 30);
+ g.drawString("+30s", g.getWidth() - 30, g.getHeight() / 2);
+ g.drawString("+5s", g.getWidth() - 30, g.getHeight() - 30);
+
+ g.setFontAlign(0, 0); // center font
+ g.setFont("6x8", 3);
+ // draw the current counter value
+
+ g.drawString(msToTime(counter * 1000), g.getWidth() / 2 - 30, g.getHeight() / 2);
+ // optional - this keeps the watch LCD lit up
+ g.flip();
+}
+
+function countDown() {
+ if (counter > 0) {
+ if (started) {
+ counter--;
+ drawInterface();
+ }
+ } else {
+ if (started) {
+ Bangle.buzz();
+ }
+ }
+}
+
+setWatch((p) => {
+ if (p.time - p.lastTime < 0.1) {
+ counter = 0;
+ started = false;
+ } else {
+ counter += 60 * 5;
+ }
+ drawInterface();
+}, BTN1, { repeat: true });
+
+setWatch(() => {
+ counter += 30;
+ drawInterface();
+}, BTN2, { repeat: true });
+
+setWatch(() => {
+ counter += 5;
+ drawInterface();
+}, BTN3, { repeat: true });
+
+Bangle.on('touch', function (button) {
+ started = !started;
+});
+
+var interval = setInterval(countDown, 1000);
+drawInterface();
\ No newline at end of file
diff --git a/apps/chrono/chrono.png b/apps/chrono/chrono.png
new file mode 100644
index 000000000..c1aaf180d
Binary files /dev/null and b/apps/chrono/chrono.png differ
diff --git a/apps/clck3x2/ChangeLog b/apps/clck3x2/ChangeLog
deleted file mode 100644
index 7819dbe2a..000000000
--- a/apps/clck3x2/ChangeLog
+++ /dev/null
@@ -1 +0,0 @@
-0.02: Modified for use with new bootloader and firmware
diff --git a/apps/clck3x2/clock3x2.json b/apps/clck3x2/clock3x2.json
deleted file mode 100644
index 2d445787e..000000000
--- a/apps/clck3x2/clock3x2.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name":"Clock 3x2 Pix",
- "type":"clock",
- "icon":"*clck3x2",
- "src":"-clck3x2"
-}
diff --git a/apps/clickms/click-master.json b/apps/clickms/click-master.json
deleted file mode 100644
index 6a2874259..000000000
--- a/apps/clickms/click-master.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Click Master",
- "icon": "*clickms",
- "src":"-clickms"
-}
diff --git a/apps/cliock/ChangeLog b/apps/cliock/ChangeLog
new file mode 100644
index 000000000..081a638f6
--- /dev/null
+++ b/apps/cliock/ChangeLog
@@ -0,0 +1 @@
+0.07: Submitted to App Loader
diff --git a/apps/cliock/app-icon.js b/apps/cliock/app-icon.js
new file mode 100644
index 000000000..fab023339
--- /dev/null
+++ b/apps/cliock/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("kEgwkBiIA/ACBhLB6gqKB6g//B6I4DiDqCB40QB4MBAoIXDB40BAIIPNG44PLAoQvMB5RPEB5JvEBAav1f7wA/ABoA=="))
diff --git a/apps/cliock/app.js b/apps/cliock/app.js
new file mode 100644
index 000000000..20086464e
--- /dev/null
+++ b/apps/cliock/app.js
@@ -0,0 +1,51 @@
+var fontsize = 3;
+var locale = require("locale");
+var marginTop = 40;
+var flag = false;
+var WeekDays = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
+
+function drawAll(){
+ g.clear();
+ Bangle.loadWidgets();
+ Bangle.drawWidgets();
+ updateTime();
+ updateRest(new Date());
+}
+
+function updateRest(now){
+ let date = locale.date(now,false);
+ writeLine(WeekDays[now.getDay()],1);
+ writeLine(date,2);
+}
+function updateTime(){
+ if (!Bangle.isLCDOn()) return;
+ let now = new Date();
+ let h = now.getHours();
+ let m = now.getMinutes();
+ h = h>=10?h:"0"+h;
+ m = m>=10?m:"0"+m;
+ writeLine(h+":"+m,0);
+ writeLine(flag?" ":"_",3);
+ flag = !flag;
+ if(now.getMinutes() == 0)
+ updateRest(now);
+}
+function writeLineStart(line){
+ g.drawString(">",4,marginTop+line*30);
+}
+function writeLine(str,line){
+ g.setFont("6x8",fontsize);
+ g.setColor(0,1,0);
+ g.setFontAlign(-1,-1);
+ g.clearRect(0,marginTop+line*30,((str.length+1)*20),marginTop+25+line*30);
+ writeLineStart(line);
+ g.drawString(str,25,marginTop+line*30);
+}
+
+drawAll();
+Bangle.on('lcdPower',function(on) {
+ if (on)
+ drawAll();
+});
+var click = setInterval(updateTime, 1000);
+setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
diff --git a/apps/cliock/app.png b/apps/cliock/app.png
new file mode 100644
index 000000000..4ad2d056d
Binary files /dev/null and b/apps/cliock/app.png differ
diff --git a/apps/clock2x3/ChangeLog b/apps/clock2x3/ChangeLog
new file mode 100644
index 000000000..88876affa
--- /dev/null
+++ b/apps/clock2x3/ChangeLog
@@ -0,0 +1,3 @@
+0.02: Modified for use with new bootloader and firmware
+0.03: Added 'reset' so we don't get the font color from widgets
+0.04: Changed name from clck3x2 to clock2x3
diff --git a/apps/clck3x2/clock3x2.js b/apps/clock2x3/clock2x3-app.js
similarity index 95%
rename from apps/clck3x2/clock3x2.js
rename to apps/clock2x3/clock2x3-app.js
index fdb38db2a..511a7662b 100644
--- a/apps/clck3x2/clock3x2.js
+++ b/apps/clock2x3/clock2x3-app.js
@@ -57,9 +57,9 @@ const pixels = [[[0,0], // digit on/off pixels
let idTimeout = null;
function drawTime() {
- g.clear();
+ g.clear(1);
Bangle.drawWidgets();
-
+ g.reset();
let d = Date();
let h = d.getHours();
let m = d.getMinutes();
@@ -76,8 +76,7 @@ function drawTime() {
}
let t = d.getSeconds()*1000 + d.getMilliseconds();
- let delta = (60000 - t) % 60000; // time till next minute
- idTimeout = setTimeout(drawTime, delta);
+ idTimeout = setTimeout(drawTime, 60000 - t); // time till next minute
}
// special function to handle display switch on
diff --git a/apps/clck3x2/clock3x2-icon.js b/apps/clock2x3/clock2x3-icon.js
similarity index 61%
rename from apps/clck3x2/clock3x2-icon.js
rename to apps/clock2x3/clock2x3-icon.js
index 5ef420d1e..961e259fb 100644
--- a/apps/clck3x2/clock3x2-icon.js
+++ b/apps/clock2x3/clock2x3-icon.js
@@ -1 +1 @@
-require("heatshrink").decompress(atob("mEwgRC/AH4A/gED/k/5/wh/wgAFCBcg7NgAVBh/zDoYLkHaAFqAH4A/AH4AW"));
+require("heatshrink").decompress(atob("mEwgRC/AH4A/gED/k/5/wh/wgAFCBcg7NgAVBh/zDoYLkHaAFqAH4A/AH4AW"))
diff --git a/apps/clck3x2/clock3x2.png b/apps/clock2x3/clock2x3.png
similarity index 100%
rename from apps/clck3x2/clock3x2.png
rename to apps/clock2x3/clock2x3.png
diff --git a/apps/clotris/clock-tris.json b/apps/clotris/clock-tris.json
deleted file mode 100644
index ddbb7d10c..000000000
--- a/apps/clotris/clock-tris.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Clock-Tris",
- "icon":"*clotris",
- "src":"-clotris"
-}
\ No newline at end of file
diff --git a/apps/compass/compass.json b/apps/compass/compass.json
deleted file mode 100644
index 7abc9a733..000000000
--- a/apps/compass/compass.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Compass","type":"app",
- "icon":"*compass",
- "src":"-compass"
-}
diff --git a/apps/ctrclk/app.json b/apps/ctrclk/app.json
deleted file mode 100644
index 68fbfdb70..000000000
--- a/apps/ctrclk/app.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Centerclock",
- "type": "clock",
- "icon": "*ctrclk",
- "src": "-ctrclk"
-}
diff --git a/apps/cube/cube.json b/apps/cube/cube.json
deleted file mode 100644
index 440a78c72..000000000
--- a/apps/cube/cube.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name":"Cube",
- "type":"app",
- "icon":"*cube",
- "src":"-cube"
- }
diff --git a/apps/daysl/ChangeLog b/apps/daysl/ChangeLog
new file mode 100644
index 000000000..c3faf0092
--- /dev/null
+++ b/apps/daysl/ChangeLog
@@ -0,0 +1,3 @@
+0.01: New Widget!
+0.02: Improved calculation, new image for app
+0.03: Improved display of number
diff --git a/apps/daysl/app-icon.js b/apps/daysl/app-icon.js
new file mode 100644
index 000000000..bdc0da744
--- /dev/null
+++ b/apps/daysl/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgmIAH4A/AH4A/AEEAAAgGOC/4XLAgoGIDgYXTwEIBY4JEAw8YCIOAEY4+EAwwTCL44XNO5IX/C6i6LC8YABa5AXOF67vIwA5DAw5GDMhg7HjAXWIwQLFZIoGNC/4XKAH4A/AH4A/ADoA="))
\ No newline at end of file
diff --git a/apps/daysl/app.js b/apps/daysl/app.js
new file mode 100644
index 000000000..56f85e615
--- /dev/null
+++ b/apps/daysl/app.js
@@ -0,0 +1,67 @@
+g.clear();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+const storage = require('Storage');
+let settings;
+
+function updateSettings() {
+ storage.write('daysleft.json', settings);
+}
+
+function resetSettings() {
+ settings = {
+ day : 17,
+ month : 6,
+ year: 1981
+ };
+ updateSettings();
+}
+
+settings = storage.readJSON('daysleft.json',1);
+if (!settings) resetSettings();
+
+function showMenu() {
+ const datemenu = {
+ '': {
+ 'title': 'Set Date',
+ 'predraw': function() {
+ datemenu.Date.value = settings.day;
+ datemenu.Month.value = settings.month;
+ datemenu.Year.value = settings.year;
+ }
+ },
+ 'Day': {
+ value: settings.day,
+ min: 1,
+ max: 31,
+ step: 1,
+ onchange: v => {
+ settings.day = v;
+ updateSettings();
+ }
+ },
+ 'Month': {
+ value: settings.month,
+ min: 1,
+ max: 12,
+ step: 1,
+ onchange: v => {
+ settings.month = v;
+ updateSettings();
+ }
+ },
+ 'Year': {
+ value: settings.year,
+ step: 1,
+ onchange: v => {
+ settings.year = v;
+ updateSettings();
+ }
+ }
+ };
+ datemenu['-Exit-'] = ()=>{load();};
+ return E.showMenu(datemenu);
+}
+
+showMenu();
\ No newline at end of file
diff --git a/apps/daysl/app.png b/apps/daysl/app.png
new file mode 100644
index 000000000..42839ae59
Binary files /dev/null and b/apps/daysl/app.png differ
diff --git a/apps/daysl/widget.js b/apps/daysl/widget.js
new file mode 100644
index 000000000..4a32d5f26
--- /dev/null
+++ b/apps/daysl/widget.js
@@ -0,0 +1,84 @@
+const storage = require('Storage');
+let settings;
+let height = 23;
+let width = 34;
+
+var debug = 0; //1 = show debug info
+
+//write settings to file
+function updateSettings() {
+ storage.write('daysleft.json', settings);
+}
+
+//Define standard settings
+function resetSettings() {
+ settings = {
+ day : 17,
+ month : 6,
+ year: 2020
+ };
+ updateSettings();
+}
+
+settings = storage.readJSON('daysleft.json',1); //read storage
+if (!settings) resetSettings(); //if settings file was not found, set to standard
+
+var dd = settings.day,
+ mm = settings.month-1, //-1 because month is zero-based
+ yy = settings.year;
+
+const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds
+const targetDate = new Date(yy, mm, dd); //is 00:00
+const today = new Date(); //includes current time
+
+const currentYear = today.getFullYear();
+const currentMonth = today.getMonth();
+const currentDay = today.getDate();
+const todayMorning = new Date (currentYear, currentMonth, currentDay, 0, 0, 0); //create date object with today, but 00:00:00
+
+const diffDays = (targetDate - todayMorning) / oneDay; //calculate day difference
+
+function drawWidget() {
+ if (debug == 1) g.drawRect(this.x,this.y,this.x+width,this.y+height); //draw rectangle around widget area
+ g.reset();
+
+ //define font size and string position
+ //small if number has more than 3 digits (positive number)
+ if (diffDays >= 1000) {
+ g.setFont("6x8", 1);
+ g.drawString(diffDays,this.x+10,this.y+7);
+ }
+ //large if number has 3 digits (positive number)
+ if (diffDays <= 999 && diffDays >= 100) {
+ g.setFont("6x8", 2);
+ g.drawString(diffDays,this.x,this.y+4);
+ }
+ //large if number has 2 digits (positive number)
+ if (diffDays <= 99 && diffDays >= 10) {
+ g.setFont("6x8", 2);
+ g.drawString(diffDays,this.x+6,this.y+4);
+ }
+ //large if number has 1 digit (positive number)
+ if (diffDays <= 9 && diffDays >= 0) {
+ g.setFont("6x8", 2);
+ g.drawString(diffDays,this.x+13,this.y+4);
+ }
+ //large if number has 1 digit (negative number)
+ if (diffDays <= -1 && diffDays >= -9) {
+ g.setFont("6x8", 2);
+ g.drawString(diffDays,this.x+5,this.y+4);
+ }
+ //large if number has 2 digits (negative number)
+ if (diffDays <= -10 && diffDays >= -99) {
+ g.setFont("6x8", 2);
+ g.drawString(diffDays,this.x,this.y+4);
+ }
+ //large if number has 3 digits or more (negative number)
+ if (diffDays <= -100) {
+ g.setFont("6x8", 1);
+ g.drawString(diffDays,this.x,this.y+7);
+ }
+}
+
+//draw widget
+WIDGETS["daysl"]={area:"tl",width:width,draw:drawWidget};
\ No newline at end of file
diff --git a/apps/dclock/ChangeLog b/apps/dclock/ChangeLog
new file mode 100644
index 000000000..edf7da4c2
--- /dev/null
+++ b/apps/dclock/ChangeLog
@@ -0,0 +1,9 @@
+0.01: branched from simple clock and added seconds
+0.02: add timestamp (tst)
+0.03: fix timestamp round to whole number
+0.04: add iso datetime and move day of the week (d) / month names (m)
+0.05: add beats (@)
+0.06: tidy up
+0.07: add days in current month (md) and days since new moon (l)
+0.08: update icon
+0.09: Use localised month and day of the week from locale
diff --git a/apps/dclock/clock-dev-icon.js b/apps/dclock/clock-dev-icon.js
new file mode 100644
index 000000000..f36dcaee3
--- /dev/null
+++ b/apps/dclock/clock-dev-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkEIf4A5/8wgf/AwUB/8gh/zA4QMCl/xA4cAichgIaBiEDgMgmECDQMAkMA+EgiYvDkQJBkcQgMQDwMggUiiECG4MikEBmQWCgURiEREQIXBCIMxkIIBAoMSiQ4BGoIABKgPykRSBI4JfC+c/iARBl8zmBfEAAUvIgIAUkbAtgalB+ADDBIKSBHgUgmYJCAAa6BmCoBAYMiBIMRC4UQmEAAoQvFmUDAYUSmcxWIKMBEQKrBOw0yh8wmcyj4nBIYQDB+cwBAQA/ABUxgUDkBqBgchkMiiUikMRgSOBkR3BkEhC4MgiQHBiADBC4UQAYMRiUxkECAAITBC4MSiUQF4MTiQTBBAIDBkcCiMxkUTAYIvCAH4A/AH4AKiIPPgMxiESgUQgECgMBdAMiiUgC48ikUBiEBiIXDGQURiIbBF48RkAvCEwIvCkERgQMBRHpDBOoRhBNoJOBJIkiKYMjgcTOoMhLQMQmMDDIMjQQInEC4MhiUSkQHCC4MAkAXCiUjiZ5UiR5jLwLaBAQJ1BAgIAMCgMxMwMgkciAoMjC5pqBRwPxCoMiiUyGBsgiBBBiESVAKzBf+YACA=="))
\ No newline at end of file
diff --git a/apps/dclock/clock-dev.js b/apps/dclock/clock-dev.js
new file mode 100644
index 000000000..d2c08726a
--- /dev/null
+++ b/apps/dclock/clock-dev.js
@@ -0,0 +1,112 @@
+var locale = require("locale");
+/* jshint esversion: 6 */
+const timeFontSize = 4;
+const dateFontSize = 3;
+const smallFontSize = 2;
+const font = "6x8";
+
+const xyCenter = g.getWidth() / 2;
+const yposTime = 50;
+const yposDate = 85;
+const yposTst = 115;
+const yposDml = 170;
+const yposDayMonth = 195;
+const yposGMT = 220;
+
+// Check settings for what type our clock should be
+var is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
+
+function getUTCTime(d) {
+ return d.toUTCString().split(' ')[4].split(':').map(function(d){return Number(d)});
+}
+
+function drawSimpleClock() {
+ // get date
+ var d = new Date();
+ var da = d.toString().split(" ");
+ var dutc = getUTCTime(d);
+
+ g.reset(); // default draw styles
+ // drawSting centered
+ g.setFontAlign(0, 0);
+
+ // draw time
+ var time = da[4].split(":");
+ var hours = time[0],
+ minutes = time[1],
+ seconds = time[2];
+
+ var meridian = "";
+ if (is12Hour) {
+ hours = parseInt(hours,10);
+ meridian = "AM";
+ if (hours == 0) {
+ hours = 12;
+ meridian = "AM";
+ } else if (hours >= 12) {
+ meridian = "PM";
+ if (hours>12) hours -= 12;
+ }
+ hours = (" "+hours).substr(-2);
+ }
+
+ // Time
+ g.setFont(font, timeFontSize);
+ g.drawString(`${hours}:${minutes}:${seconds}`, xyCenter, yposTime, true);
+ g.setFont(font, smallFontSize);
+ g.drawString(meridian, xyCenter + 102, yposTime + 10, true);
+
+ // Date String
+ g.setFont(font, dateFontSize);
+ g.drawString(`${d.getFullYear()}-${d.getMonth()+1}-${d.getDate()}`, xyCenter, yposDate, true);
+
+ // Timestamp
+ var tst = Math.round(d.getTime());
+ g.setFont(font, smallFontSize);
+ g.drawString(`tst:${tst}`, xyCenter, yposTst, true);
+
+ //Days in month
+ var dom = new Date(d.getFullYear(), d.getMonth()+1, 0).getDate();
+
+ //Days since full moon
+ var knownnew = new Date(2020,02,24,09,28,0);
+
+ // Get millisecond difference and divide down to cycles
+ var cycles = (d.getTime()-knownnew.getTime())/1000/60/60/24/29.53;
+
+ // Multiply decimal component back into days since new moon
+ var sincenew = (cycles % 1)*29.53;
+
+ // Draw days in month and sime since new moon
+ g.setFont(font, smallFontSize);
+ g.drawString(`md:${dom} l:${sincenew.toFixed(2)}`, xyCenter, yposDml, true);
+
+ // draw Month name, Day of the week and beats
+ var beats = Math.floor((((dutc[0] + 1) % 24) + dutc[1] / 60 + dutc[2] / 3600) * 1000 / 24);
+ g.setFont(font, smallFontSize);
+ g.drawString(`m:${locale.month(d,true)} d:${locale.dow(d,true)} @${beats}`, xyCenter, yposDayMonth, true);
+
+ // draw gmt
+ var gmt = da[5];
+ g.setFont(font, smallFontSize);
+ g.drawString(gmt, xyCenter, yposGMT, true);
+}
+
+// handle switch display on by pressing BTN1
+Bangle.on('lcdPower', function(on) {
+ if (on) drawSimpleClock();
+});
+
+// clean app screen
+g.clear();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+// refesh every 100 milliseconds
+setInterval(drawSimpleClock, 100);
+
+// draw now
+drawSimpleClock();
+
+// Show launcher when middle button pressed
+setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
\ No newline at end of file
diff --git a/apps/dclock/clock-dev.png b/apps/dclock/clock-dev.png
new file mode 100644
index 000000000..0cfbff44c
Binary files /dev/null and b/apps/dclock/clock-dev.png differ
diff --git a/apps/demoapp/app.js b/apps/demoapp/app.js
index b84839b45..cb3136196 100644
--- a/apps/demoapp/app.js
+++ b/apps/demoapp/app.js
@@ -9,6 +9,7 @@ var scenes = [
y+=step;
if (y>70) {
clearInterval(i);
+
i = undefined;
}
g.clearRect(0,y-(step+1),240,y-1);
@@ -159,10 +160,13 @@ var scenes = [
];
var sceneNo = scenes.length-1;
-var stop = scenes[sceneNo]();
-setInterval(function() {
+var stop;
+function next() {
sceneNo++;
if (sceneNo>=scenes.length) sceneNo=0;
- stop();
+ if (stop) stop();
+ clearInterval();
stop = scenes[sceneNo]();
-}, 10000);
+ setTimeout(next, 10000);
+}
+next()
diff --git a/apps/demoapp/app.json b/apps/demoapp/app.json
deleted file mode 100644
index fab184c15..000000000
--- a/apps/demoapp/app.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Demo Loop",
- "icon":"*demoapp",
- "src":"-demoapp"
-}
diff --git a/apps/dotclock/ChangeLog b/apps/dotclock/ChangeLog
new file mode 100644
index 000000000..26f95bbde
--- /dev/null
+++ b/apps/dotclock/ChangeLog
@@ -0,0 +1 @@
+0.01: Based on the Analog Clock app, minimal dot interface
\ No newline at end of file
diff --git a/apps/dotclock/clock-dot-icon.js b/apps/dotclock/clock-dot-icon.js
new file mode 100644
index 000000000..7098cb51f
--- /dev/null
+++ b/apps/dotclock/clock-dot-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwkBIf4A/AGUJyAXtACeZBCAOJh/wC6IADC4gA/XEINJC64A/AHcP+ACD/4CBTB0Ph8A+ACBAIKoKC65HKC4gA/AAfACysM5gvjTBgNKC64A/AEWZBCAXdADa4XaH4A/AAgA=="))
diff --git a/apps/dotclock/clock-dot.js b/apps/dotclock/clock-dot.js
new file mode 100644
index 000000000..a4b3f260f
--- /dev/null
+++ b/apps/dotclock/clock-dot.js
@@ -0,0 +1,162 @@
+let g;
+let Bangle;
+
+const locale = require('locale');
+const p = Math.PI / 2;
+const pRad = Math.PI / 180;
+const faceWidth = 100; // watch face radius
+let timer = null;
+let currentDate = new Date();
+let hourRadius = 60;
+let minRadius = 80;
+const centerPx = g.getWidth() / 2;
+
+const seconds = (angle) => {
+ const a = angle * pRad;
+ const x = centerPx + Math.sin(a) * faceWidth;
+ const y = centerPx - Math.cos(a) * faceWidth;
+
+ // if 15 degrees, make hour marker larger
+ const radius = (angle % 15) ? 2 : 4;
+ g.fillCircle(x, y, radius);
+};
+
+const hourDot = (angle,radius) => {
+ const a = angle * pRad;
+ const x = centerPx + Math.sin(a) * hourRadius;
+ const y = centerPx - Math.cos(a) * hourRadius;
+ g.fillCircle(x, y, radius);
+};
+
+const minDot = (angle,radius) => {
+ const a = angle * pRad;
+ const x = centerPx + Math.sin(a) * minRadius;
+ const y = centerPx - Math.cos(a) * minRadius;
+ g.fillCircle(x, y, radius);
+};
+
+const drawAll = () => {
+ g.clear();
+ currentDate = new Date();
+ // draw hands first
+ onMinute();
+ // draw seconds
+ const currentSec = currentDate.getSeconds();
+ // draw all secs
+
+ for (let i = 0; i < 60; i++) {
+ if (i > currentSec) {
+ g.setColor(0, 0, 0.6);
+ } else {
+ g.setColor(0.3, 0.3, 1);
+ }
+ seconds((360 * i) / 60);
+ }
+ onSecond();
+};
+
+const resetSeconds = () => {
+ g.setColor(0, 0, 0.6);
+ for (let i = 0; i < 60; i++) {
+ seconds((360 * i) / 60);
+ }
+};
+
+const drawMin = () => {
+ g.setColor(0.5, 0.5, 0.5);
+ for (let i = 0; i < 60; i++) {
+ minDot((360 * i) / 60,1);
+ }
+};
+
+const drawHour = () => {
+ g.setColor(0.5, 0.5, 0.5);
+ for (let i = 0; i < 12; i++) {
+ hourDot((360 * 5 * i) / 60,1);
+ }
+};
+
+const onSecond = () => {
+ g.setColor(0.3, 0.3, 1);
+ seconds((360 * currentDate.getSeconds()) / 60);
+ if (currentDate.getSeconds() === 59) {
+ resetSeconds();
+ onMinute();
+ }
+ g.setColor(1, 0.7, 0.2);
+ currentDate = new Date();
+ seconds((360 * currentDate.getSeconds()) / 60);
+ g.setColor(1, 1, 1);
+};
+
+const drawDate = () => {
+ g.reset();
+ g.setColor(1, 1, 1);
+ g.setFont('6x8', 2);
+
+ const dayString = locale.dow(currentDate, true);
+ // pad left date
+ const dateString = ((currentDate.getDate() < 10) ? '0' : '') + currentDate.getDate().toString();
+ const dateDisplay = `${dayString} ${dateString}`;
+ // console.log(`${dayString}|${dateString}`);
+ // center date
+ const l = (g.getWidth() - g.stringWidth(dateDisplay)) / 2;
+ const t = centerPx - 6 ;
+ g.drawString(dateDisplay, l, t);
+ // console.log(l, t);
+};
+const onMinute = () => {
+ if (currentDate.getHours() === 0 && currentDate.getMinutes() === 0) {
+ g.clear();
+ resetSeconds();
+ }
+ // clear existing hands
+ g.setColor(0, 0, 0);
+ hourDot((360 * currentDate.getHours()) / 12,4);
+ minDot((360 * currentDate.getMinutes()) / 60,3);
+
+ // Hour
+ drawHour();
+ // Minute
+ drawMin();
+
+ // get new date, then draw new hands
+ currentDate = new Date();
+ g.setColor(1, 0, 0);
+ // Hour
+ hourDot((360 * currentDate.getHours()) / 12,4);
+ g.setColor(1, 0.9, 0.9);
+ // Minute
+ minDot((360 * currentDate.getMinutes()) / 60,3);
+ if (currentDate.getHours() >= 0 && currentDate.getMinutes() === 0) {
+ Bangle.buzz();
+ }
+ drawDate();
+};
+
+const startTimers = () => {
+ timer = setInterval(onSecond, 1000);
+};
+
+Bangle.on('lcdPower', (on) => {
+ if (on) {
+ // g.clear();
+ drawAll();
+ startTimers();
+ Bangle.drawWidgets();
+ } else {
+ if (timer) {
+ clearInterval(timer);
+ }
+ }
+});
+
+g.clear();
+resetSeconds();
+startTimers();
+drawAll();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+// Show launcher when middle button pressed
+setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: "falling" });
diff --git a/apps/dotclock/clock-dot.png b/apps/dotclock/clock-dot.png
new file mode 100644
index 000000000..702ac9065
Binary files /dev/null and b/apps/dotclock/clock-dot.png differ
diff --git a/apps/files/files.js b/apps/files/files.js
index b5b77a7f8..31353cf96 100644
--- a/apps/files/files.js
+++ b/apps/files/files.js
@@ -31,7 +31,7 @@ function showMainMenu() {
}
function eraseApp(app) {
- E.showMessage('Erasing ' + app.name + '...');
+ E.showMessage('Erasing\n' + app.name + '...');
storage.erase(app['']);
storage.erase(app.icon);
storage.erase(app.src);
@@ -44,7 +44,7 @@ function showAppMenu(app) {
},
'< Back': () => m = showApps(),
'Erase': () => {
- E.showPrompt('Erase ' + app.name + '?').then((v) => {
+ E.showPrompt('Erase\n' + app.name + '?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
eraseApp(app);
@@ -66,10 +66,10 @@ function showApps() {
'< Back': () => m = showMainMenu(),
};
- var list = storage.list().filter((a)=> {
- return a[0]=='+' && a !== '+setting';
+ var list = storage.list(/\.info$/).filter((a)=> {
+ return a !== 'setting.info';
}).sort().map((app) => {
- var ret = storage.readJSON(app);
+ var ret = storage.readJSON(app,1)||{};
ret[''] = app;
return ret;
});
diff --git a/apps/files/files.json b/apps/files/files.json
deleted file mode 100644
index db43a7cdc..000000000
--- a/apps/files/files.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"App Manager","type":"app",
- "icon":"*files",
- "src":"-files"
-}
diff --git a/apps/flagrse/app-icon.js b/apps/flagrse/app-icon.js
new file mode 100644
index 000000000..68d95b518
--- /dev/null
+++ b/apps/flagrse/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AJmUyF/4Adstl1ovvAAIvvGNQvGGNAvIGMwvKGMgvMGMQvOGMAvQGLwvSGLgvUGLQvWGLAvYGKwv/R+zvtFrAvUFrQvSFrgvQFrwvOFsAvMFsQvKFsgvIFswvGFtAvEFtQABmUyF1gv/F/4v/F/4v/F/4v/F/4A/AH4A/AAwA="))
diff --git a/apps/flagrse/app.js b/apps/flagrse/app.js
new file mode 100644
index 000000000..e7e8e2445
--- /dev/null
+++ b/apps/flagrse/app.js
@@ -0,0 +1,57 @@
+
+function redraw() {
+ var img = require("heatshrink").decompress(atob("sFgxH+AH4A/AH4A/AH4A/AH4A/AH4A/AH4A/ACcyAARD/L/5f/If5f/AHdlAAWtIn5ffAAJG/L75h/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh/L8Jh+L8Rh8L8hh6L8xh4L9Bh2L9Rh0L9hhyL9xhwL+BhuL+RhsL+hhqL/5f/Lvpf0LtRfyLthfwLtxfuLuBfsLuRfqLuhfoLuxfmLvBfkLvRfiLvhfgLvxfeLn5fdLX5fdLH5fdK35fdKn5fdKX5fdKH5fdJ35fdJn5fdJX5fdJH5fdI35fdIn4AamQACIf5f/L/5D/L/5f/If5f/L/5D/L/5f/If5f/L/5D/L/5f/If5f/L/5D/L/5f/If5f/L/5D/L/5f/If5f/L/5D/L/4A/AH4A/AH4A/AH4A/AH4A/AH4A/AH4AnA"));
+ g.clear();
+ g.drawImage(img, 120-96, 120-96, {scale:2});
+}
+
+ // Code for button (Puck.js)
+ var busy = false;
+
+var lastTry = getTime();
+
+function flag() {
+ E.showMessage("Working...");
+ if (busy && lastTry+5 Phone
+
+## show toast
+
+```
+{ "t": "info", "msg": "message" }
+```
+
+t can be one of "info", "warn", "error"
+
+## report battery level
+
+```
+{ "t": "status", "bat": 30, "volt": 30 }
+```
+
+* bat is in range 0 to 100
+* volt is optional and should be greater than 0
+
+## find phone
+
+```
+{ "t": "findPhone", "n": true }
+```
+
+n is an boolean and toggles the find phone function
+
+## control music player
+
+```
+{ "t": "music", "n": "play" }
+```
+
+n can be one of "play", "pause", "playpause", "next", "previous", "volumeup", "volumedown", "forward", "rewind"
+
+## control phone call
+
+```
+{ "t": "call", "n": "accept"}
+```
+
+n can be one of "accept", "end", "incoming", "outcoming", "reject", "start", "ignore"
+
+## react to notifications
+
+Send a response to a notification from phone
+
+```
+{
+ "t": "notify",
+ "n": "dismiss",
+ "id": 2,
+ "tel": "+491234",
+ "msg": "message",
+}
+```
+
+* n can be one of "dismiss", "dismiss all", "open", "mute", "reply"
+* id, tel and message are optional
+
+# Phone -> Watch
+
+## show notification
+
+```
+{
+ "t": "notify",
+ "id": 2,
+ "src": "app",
+ "title": "titel",
+ "subject": "subject",
+ "body": "message body",
+ "sender": "sender",
+ "tel": "+491234"
+ }
+```
+
+## notification deleted
+
+This event is send when the user skipped a notification
+
+```
+{ "t": "notify-", "id": 2 }
+```
+
+## set alarm
+
+```
+{
+ "t": "alarm",
+ "d": [
+ { "h": 13, "m": 37 },
+ { "h": 8, "m": 0 }
+ ]
+}
+```
+
+## call state changed
+
+```
+{
+ "t": "call",
+ "cmd": "accept",
+ "name": "name",
+ "number": "+491234"
+}
+```
+
+cmd can be one of "", "undefined", "accept", "incoming", "outgoing", "reject", "start", "end"
+
+## music state changed
+
+```
+{
+ "t": "musicstate",
+ "state": "play",
+ "position": 40,
+ "shuffle": 0,
+ "repeat": 1
+}
+```
+
+## set music info
+
+```
+{
+ "t": "musicinfo",
+ "artist": "artist",
+ "album": "album",
+ "track": "track",
+ "dur": 1,
+ "c": 2,
+ "n" 3
+}
+```
+
+* dur is the duration of the track
+* c is the track count
+* n is the track number
+
+## find device
+
+```
+{
+ "t": "find",
+ "n": true
+}
+```
+
+n toggles find device functionality
+
+## set constant vibration
+
+```
+{
+ "t": "vibrate",
+ "n": 2
+}
+```
+
+n is the intensity
+
+## send weather
+
+```
+{
+ "t": "weather",
+ "temp": 10,
+ "hum": 71,
+ "txt": "condition",
+ "wind": 13,
+ "loc": "location"
+}
+```
+
+* hum is the humidity
+* txt is the weather condition
+* loc is the location
diff --git a/apps/gbridge/app.js b/apps/gbridge/app.js
deleted file mode 100644
index 45dc0e33d..000000000
--- a/apps/gbridge/app.js
+++ /dev/null
@@ -1,19 +0,0 @@
-function gb(j) {
- Bluetooth.println(JSON.stringify(j));
-}
-
-var mainmenu = {
- "" : { "title" : "Gadgetbridge" },
- "Connected" : { value : NRF.getSecurityStatus().connected },
- "Find Phone" : function() { E.showMenu(findPhone); },
- "Exit" : ()=> {load();},
-};
-
-var findPhone = {
- "" : { "title" : "-- Find Phone --" },
- "On" : _=>gb({t:"findPhone",n:true}),
- "Off" : _=>gb({t:"findPhone",n:false}),
- "< Back" : function() { E.showMenu(mainmenu); },
-};
-
-E.showMenu(mainmenu);
diff --git a/apps/gbridge/app.json b/apps/gbridge/app.json
deleted file mode 100644
index f5c8f3991..000000000
--- a/apps/gbridge/app.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Gadgetbridge",
- "icon":"*gbridge",
- "src":"-gbridge"
-}
diff --git a/apps/gbridge/settings.js b/apps/gbridge/settings.js
new file mode 100644
index 000000000..723c9cae9
--- /dev/null
+++ b/apps/gbridge/settings.js
@@ -0,0 +1,21 @@
+(function(back) {
+ function gb(j) {
+ Bluetooth.println(JSON.stringify(j));
+ }
+
+ var mainmenu = {
+ "" : { "title" : "Gadgetbridge" },
+ "Connected" : { value : NRF.getSecurityStatus().connected?"Yes":"No" },
+ "Find Phone" : function() { E.showMenu(findPhone); },
+ "< Back" : back,
+ };
+
+ var findPhone = {
+ "" : { "title" : "-- Find Phone --" },
+ "On" : _=>gb({t:"findPhone",n:true}),
+ "Off" : _=>gb({t:"findPhone",n:false}),
+ "< Back" : function() { E.showMenu(mainmenu); },
+ };
+
+ E.showMenu(mainmenu);
+})
diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js
index 21545c5f0..3f9c7053f 100644
--- a/apps/gbridge/widget.js
+++ b/apps/gbridge/widget.js
@@ -1,118 +1,196 @@
-(function() {
- var musicState = "stop";
- var musicInfo = {"artist":"","album":"","track":""};
- var scrollPos = 0;
- function gb(j) {
- Bluetooth.println(JSON.stringify(j));
+(() => {
+
+ const state = {
+ music: "stop",
+
+ musicInfo: {
+ artist: "",
+ album: "",
+ track: ""
+ },
+
+ scrollPos: 0
+ };
+
+ function gbSend(message) {
+ Bluetooth.println(JSON.stringify(message));
}
- function show(size,render) {
+
+ function showNotification(size, render) {
var oldMode = Bangle.getLCDMode();
+
Bangle.setLCDMode("direct");
- g.setClipRect(0,240,239,319);
- g.setColor("#404040");
- g.fillRect(1,241,238,318);
- render(320-size);
+ g.setClipRect(0, 240, 239, 319);
+ g.setColor("#222222");
+ g.fillRect(1, 241, 238, 318);
+
+ render(320 - size);
+
g.setColor("#ffffff");
- g.fillRect(0,240,1,319);
- g.fillRect(238,240,239,319);
- g.fillRect(2,318,238,319);
+ g.fillRect(0, 240, 1, 319);
+ g.fillRect(238, 240, 239, 319);
+ g.fillRect(2, 318, 238, 319);
+
Bangle.setLCDPower(1); // light up
Bangle.setLCDMode(oldMode); // clears cliprect
+
function anim() {
- scrollPos-=2;
- if (scrollPos<-size) scrollPos=-size;
- Bangle.setLCDOffset(scrollPos);
- if (scrollPos>-size) setTimeout(anim,10);
- }
- anim();
- }
- function hide() {
- function anim() {
- scrollPos+=4;
- if (scrollPos>0) scrollPos=0;
- Bangle.setLCDOffset(scrollPos);
- if (scrollPos<0) setTimeout(anim,10);
+ state.scrollPos -= 2;
+ if (state.scrollPos < -size) {
+ state.scrollPos = -size;
+ }
+ Bangle.setLCDOffset(state.scrollPos);
+ if (state.scrollPos > -size) setTimeout(anim, 15);
}
anim();
}
- Bangle.on('touch',function() {
- if (scrollPos) hide();
- });
- Bangle.on('swipe',function(dir) {
- if (musicState=="play") {
- gb({t:"music",n:dir>0?"next":"previous"});
+ function hideNotification() {
+ function anim() {
+ state.scrollPos += 4;
+ if (state.scrollPos > 0) state.scrollPos = 0;
+ Bangle.setLCDOffset(state.scrollPos);
+ if (state.scrollPos < 0) setTimeout(anim, 10);
}
- });
- gb({t:"status",bat:E.getBattery()});
+ anim();
+ }
- global.GB = function(j) {
- switch (j.t) {
+ function handleNotificationEvent(event) {
+
+ // split text up at word boundaries
+ var txt = event.body.split("\n");
+ var MAXCHARS = 38;
+ 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));
+ }
+ }
+
+ showNotification(80, (y) => {
+
+ // TODO: icon based on src?
+ var x = 120;
+ g.setFontAlign(0, 0);
+ g.setFont("6x8", 1);
+ g.setColor("#40d040");
+ g.drawString(event.src, x, y + 7);
+
+ g.setColor("#ffffff");
+ g.setFont("6x8", 2);
+ if (event.title)
+ g.drawString(event.title.slice(0,17), x, y + 25);
+
+ g.setFont("6x8", 1);
+ g.setColor("#ffffff");
+ g.setFontAlign(-1, -1);
+ g.drawString(txt.join("\n"), 10, y + 40);
+ });
+
+ Bangle.buzz();
+ }
+
+ function handleMusicStateUpdate(event) {
+ state.music = event.state
+
+ if (state.music == "play") {
+ showNotification(40, (y) => {
+ g.setColor("#ffffff");
+ g.drawImage(require("heatshrink").decompress(atob("jEYwILI/EAv/8gP/ARcMgOAASN8h+A/kfwP8n4CD/E/gHgjg/HA=")), 8, y + 8);
+
+ g.setFontAlign(-1, -1);
+ var x = 40;
+ g.setFont("4x6", 2);
+ g.setColor("#ffffff");
+ g.drawString(state.musicInfo.artist, x, y + 8);
+
+ g.setFont("6x8", 1);
+ g.setColor("#ffffff");
+ g.drawString(state.musicInfo.track, x, y + 22);
+ });
+ }
+
+ if (state.music == "pause") {
+ hideNotification();
+ }
+ }
+
+ function handleCallEvent(event) {
+
+ if (event.cmd == "accept") {
+ showNotification(40, (y) => {
+ g.setColor("#ffffff");
+ g.drawImage(require("heatshrink").decompress(atob("jEYwIMJj4CCwACJh4CCCIMOAQMGAQMHAQMDAQMBCIMB4PwgHz/EAn4CBj4CBg4CBgACCAAw=")), 8, y + 8);
+
+ g.setFontAlign(-1, -1);
+ var x = 40;
+ g.setFont("4x6", 2);
+ g.setColor("#ffffff");
+ g.drawString(event.name, x, y + 8);
+
+ g.setFont("6x8", 1);
+ g.setColor("#ffffff");
+ g.drawString(event.number, x, y + 22);
+ });
+
+ Bangle.buzz();
+ }
+ }
+
+ global.GB = (event) => {
+ switch (event.t) {
case "notify":
- show(80,function(y) {
- // TODO: icon based on src?
- var x = 120;
- g.setFontAlign(0,0);
- g.setFont("6x8",1);
- g.setColor("#40d040");
- g.drawString(j.src,x,y+7);
- g.setColor("#ffffff");
- g.setFont("6x8",2);
- g.drawString(j.title,x,y+25);
- g.setFont("6x8",1);
- g.setColor("#b0b0b0");
- // split text up a word boundaries
- var txt = j.body.split("\n");
- var MAXCHARS = 38;
- for (var i=0;iMAXCHARS) {
- 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));
- }
- }
- g.setFontAlign(-1,-1);
- g.drawString(txt.join("\n"),10,y+40);
- Bangle.buzz();
- });
- break;
+ handleNotificationEvent(event);
+ break;
case "musicinfo":
- musicInfo = j;
+ state.musicInfo = event;
break;
case "musicstate":
- musicState = j.state;
- if (musicState=="play")
- show(40,function(y) {
- g.setColor("#ffffff");
- g.drawImage( require("heatshrink").decompress(atob("jEYwILI/EAv/8gP/ARcMgOAASN8h+A/kfwP8n4CD/E/gHgjg/HA=")),8,y+8);
- g.setFontAlign(-1,-1);
- g.setFont("6x8",1);
- var x = 40;
- g.setFont("4x6",2);
- g.setColor("#ffffff");
- g.drawString(musicInfo.artist,x,y+8);
- g.setFont("6x8",1);
- g.setColor("#c0c0c0");
- g.drawString(musicInfo.track,x,y+22);
- });
- if (musicState=="pause")
- hide();
- break;
+ handleMusicStateUpdate(event);
+ break;
+ case "call":
+ handleCallEvent(event);
+ break;
}
};
+ // Touch control
+ Bangle.on("touch", () => {
+ if (state.scrollPos) {
+ hideNotification();
+ }
+ });
+ Bangle.on("swipe", (dir) => {
+ if (state.music == "play") {
+ const command = dir > 0 ? "next" : "previous"
+ gbSend({ t: "music", n: command });
+ }
+ });
-var xpos = WIDGETPOS.tl;
-WIDGETPOS.tl+=24;
-WIDGETS["gbridgew"]={draw:function() {
- g.drawImage(require("heatshrink").decompress(atob("i0WwgHExAABCIwJCBYwJEBYkIBQ2ACgvzCwoECx/z/AKDD4WD+YLBEIYKCx//+cvnAKCBwU/mc4/8/HYv//Ev+Y4EEAePn43DBQkzn4rCEIoABBIwKHO4cjmczK42I6mqlqEEBQeIBQaDED4IgDUhi6KaBbmIA==")),xpos+1,1);
- g.setColor(1,1,1);
-}};
+ function draw() {
+ g.setColor(-1);
+ if (NRF.getSecurityStatus().connected)
+ g.drawImage(require("heatshrink").decompress(atob("i0WwgHExAABCIwJCBYwJEBYkIBQ2ACgvzCwoECx/z/AKDD4WD+YLBEIYKCx//+cvnAKCBwU/mc4/8/HYv//Ev+Y4EEAePn43DBQkzn4rCEIoABBIwKHO4cjmczK42I6mqlqEEBQeIBQaDED4IgDUhi6KaBbmIA==")), this.x + 1, this.y + 1);
+ else
+ g.drawImage(require("heatshrink").decompress(atob("i0WwQFC1WgAgYFDAgIFClQFCwEK1W/AoIPB1f+CAMq1f7/WqwQPB/fq1Gq1/+/4dC/2/CAIaB/YbBAAO///qAoX/B4QbBDQQ7BDQQrBAAWoIIIACIIIVC0ECB4cACAZiBAoRtCAoIDBA")), this.x + 1, this.y + 1);
+ }
+ function changedConnectionState() {
+ WIDGETS["gbridgew"].draw();
+ g.flip(); // turns screen on
+ }
+
+ NRF.on("connected", changedConnectionState);
+ NRF.on("disconnected", changedConnectionState);
+
+ WIDGETS["gbridgew"] = { area: "tl", width: 24, draw: draw };
+
+ gbSend({ t: "status", bat: E.getBattery() });
})();
diff --git a/apps/gesture/gesture.json b/apps/gesture/gesture.json
deleted file mode 100644
index 48903e978..000000000
--- a/apps/gesture/gesture.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Gesture Test", "type":"app",
- "icon":"*gesture",
- "src":"-gesture"
-}
diff --git a/apps/gpsinfo/ChangeLog b/apps/gpsinfo/ChangeLog
new file mode 100644
index 000000000..50d79e72d
--- /dev/null
+++ b/apps/gpsinfo/ChangeLog
@@ -0,0 +1 @@
+0.02: Ensure screen doesn't display garbage at startup
diff --git a/apps/gpsinfo/gps-info.js b/apps/gpsinfo/gps-info.js
index 334310755..f7daf245a 100644
--- a/apps/gpsinfo/gps-info.js
+++ b/apps/gpsinfo/gps-info.js
@@ -2,6 +2,7 @@ var img = require("heatshrink").decompress(atob("mEwghC/AH4AKg9wC6t3u4uVC6wWBI6t
Bangle.setGPSPower(1);
Bangle.setLCDMode("doublebuffered");
+E.showMessage("Loading..."); // avoid showing rubbish on screen
var lastFix = {
fix: 0,
diff --git a/apps/gpsinfo/gps-info.json b/apps/gpsinfo/gps-info.json
deleted file mode 100644
index b86b11d58..000000000
--- a/apps/gpsinfo/gps-info.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "GPS Info",
- "type": "app",
- "icon": "*gpsinfo",
- "src": "-gpsinfo"
-}
diff --git a/apps/gpsrec/ChangeLog b/apps/gpsrec/ChangeLog
new file mode 100644
index 000000000..9a47bdd9a
--- /dev/null
+++ b/apps/gpsrec/ChangeLog
@@ -0,0 +1,6 @@
+0.01: New App!
+0.02: Fix GPS time logging
+0.03: Fix GPS time display in gpsrec app
+0.04: Properly Fix GPS time display in gpsrec app
+0.05: Tweaks for variable size widget system
+0.06: Ensure widget update itself (fix #118) and change to using icons
diff --git a/apps/gpsrec/app-icon.js b/apps/gpsrec/app-icon.js
new file mode 100644
index 000000000..9ba966765
--- /dev/null
+++ b/apps/gpsrec/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AF/W63P54XVCigACF4IACC6QsUF44yKC44sUF5QyEC5QuWF5fPC5Yv/F/3OAYgviwNdroqCwF6wHP1V6vQwKF61eiAABGAQqBFYOkAYOr62rwADBF63V1ZOCmgvCmgvB1Wk1QEB0ml6vVGYOAF65PBDQU6F4rvH6qYB0ovXDQN66vW1hgBmhnBF5AwBAgOlSQgvR5+lC4fPFpAvECASSFd6weBABR3HSQYvpSQQvsUILWBF9XVX4OkF9bvd0ocB5/O1XOR5er0oIDF62AJoPPAYIzBF5QAFF6QoBE4OrVgKACGYIvI6GI1gvXFYN60q/D52k5ySFFwc0iEQrxfXK4TvGSQoTCwQuBAAJhDF6GIxHQ6vVGgQAESQfOTwQvZnQWBmgXDF4qSC5+kGYOr62sR4U0R6WsI4eCF5AzETwYYBwWC6AvlX4gAIR553DR5Ivhd4OCFwYvpAAwv/F7wMBF6ouLF5APHF54sMF44SNF5IsPF4gUSGQQXVAH4A/AH4A/ADY"))
diff --git a/apps/gpsrec/app-settings.json b/apps/gpsrec/app-settings.json
new file mode 100644
index 000000000..7e1c8ee72
--- /dev/null
+++ b/apps/gpsrec/app-settings.json
@@ -0,0 +1,5 @@
+{
+ "recording":false,
+ "file":0,
+ "period":1
+}
diff --git a/apps/gpsrec/app.js b/apps/gpsrec/app.js
new file mode 100644
index 000000000..58b4295a6
--- /dev/null
+++ b/apps/gpsrec/app.js
@@ -0,0 +1,110 @@
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+var settings = require("Storage").readJSON("gpsrec.json",1)||{};
+
+function getFN(n) {
+ return ".gpsrc"+n.toString(36);
+}
+
+function updateSettings() {
+ require("Storage").write("gpsrec.json", settings);
+ if (WIDGETS["gpsrec"])
+ WIDGETS["gpsrec"].reload();
+}
+
+function showMainMenu() {
+ const mainmenu = {
+ '': { 'title': 'GPS Record' },
+ 'RECORD': {
+ value: !!settings.recording,
+ format: v=>v?"On":"Off",
+ onchange: v => {
+ settings.recording = v;
+ updateSettings();
+ }
+ },
+ 'File #': {
+ value: settings.file|0,
+ min: 0,
+ max: 35,
+ step: 1,
+ onchange: v => {
+ settings.recording = false;
+ settings.file = v;
+ updateSettings();
+ }
+ },
+ 'Time Period': {
+ value: settings.period||1,
+ min: 1,
+ max: 60,
+ step: 1,
+ onchange: v => {
+ settings.recording = false;
+ settings.period = v;
+ updateSettings();
+ }
+ },
+ 'View Tracks': viewTracks,
+ '< Back': ()=>{load();}
+ };
+ return E.showMenu(mainmenu);
+}
+
+function viewTracks() {
+ const menu = {
+ '': { 'title': 'GPS Tracks' }
+ };
+ var found = false;
+ for (var n=0;n<36;n++) {
+ var f = require("Storage").open(getFN(n),"r");
+ if (f.readLine()!==undefined) {
+ menu["Track "+n] = viewTrack.bind(null,n);
+ found = true;
+ }
+ }
+ if (!found)
+ menu["No Tracks found"] = function(){};
+ menu['< Back'] = showMainMenu;
+ return E.showMenu(menu);
+}
+
+function viewTrack(n) {
+ const menu = {
+ '': { 'title': 'GPS Track '+n }
+ };
+ var trackCount = 0;
+ var trackTime;
+ var f = require("Storage").open(getFN(n),"r");
+ var l = f.readLine();
+ if (l!==undefined) {
+ var c = l.split(",");
+ trackTime = new Date(parseInt(c[0]));
+ }
+ while (l!==undefined) {
+ trackCount++;
+ // TODO: min/max/length of track?
+ l = f.readLine();
+ }
+ if (trackTime)
+ menu[" "+trackTime.toISOString().substr(0,16).replace("T"," ")] = function(){};
+ menu[trackCount+" records"] = function(){};
+ // TODO: option to draw it? Just scan through, project using min/max
+ menu['Erase'] = function() {
+ E.showPrompt("Delete Track?").then(function(v) {
+ if (v) {
+ settings.recording = false;
+ updateSettings();
+ var f = require("Storage").open(getFN(n),"r");
+ f.erase();
+ viewTracks();
+ } else
+ viewTrack(n);
+ });
+ };
+ menu['< Back'] = viewTracks;
+ return E.showMenu(menu);
+}
+
+showMainMenu();
diff --git a/apps/gpsrec/app.png b/apps/gpsrec/app.png
new file mode 100644
index 000000000..ab6d37c3b
Binary files /dev/null and b/apps/gpsrec/app.png differ
diff --git a/apps/gpsrec/interface.html b/apps/gpsrec/interface.html
new file mode 100644
index 000000000..bc8dc48e1
--- /dev/null
+++ b/apps/gpsrec/interface.html
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/gpsrec/widget.js b/apps/gpsrec/widget.js
new file mode 100644
index 000000000..2ad0cfc8c
--- /dev/null
+++ b/apps/gpsrec/widget.js
@@ -0,0 +1,67 @@
+(() => {
+ var settings = {};
+ var hasFix = false;
+ var fixToggle = false; // toggles once for each reading
+ var gpsTrack; // file for GPS track
+ var periodCtr = 0;
+
+ // draw your widget
+ function draw() {
+ if (!settings.recording) return;
+ g.reset();
+ g.drawImage(atob("GBgCAAAAAAAAAAQAAAAAAD8AAAAAAP/AAAAAAP/wAAAAAH/8C9AAAB/8L/QAAAfwv/wAAAHS//wAAAAL//gAAAAf/+AAAAAf/4AAAAL//gAAAAD/+DwAAAB/Uf8AAAAfA//AAAACAf/wAAAAAH/0AAAAAB/wAAAAAAfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),this.x,this.y);
+ if (hasFix) {
+ g.setColor("#FF0000");
+ g.drawImage(fixToggle ? atob("CgoCAAAAA0AAOAAD5AAPwAAAAAAAAAAAAAAAAA==") : atob("CgoCAAABw0AcOAHj5A8PwHwAAvgAB/wABUAAAA=="),this.x,this.y+14);
+ } else {
+ g.setColor("#0000FF");
+ if (fixToggle)
+ g.setFont("6x8").drawString("?",this.x,this.y+14);
+ }
+ }
+
+ function onGPS(fix) {
+ hasFix = fix.fix;
+ fixToggle = !fixToggle;
+ WIDGETS["gpsrec"].draw();
+ if (hasFix) {
+ periodCtr--;
+ if (periodCtr<=0) {
+ periodCtr = settings.period;
+ if (gpsTrack) gpsTrack.write([
+ fix.time.getTime(),
+ fix.lat.toFixed(5),
+ fix.lon.toFixed(5),
+ fix.alt
+ ].join(",")+"\n");
+ }
+ }
+ }
+
+ // Called by the GPS app to reload settings and decide what to do
+ function reload() {
+ settings = require("Storage").readJSON("gpsrec.json",1)||{};
+ settings.period = settings.period||1;
+ settings.file |= 0;
+
+ Bangle.removeListener('GPS',onGPS);
+ if (settings.recording) {
+ WIDGETS["gpsrec"].width = 24;
+ Bangle.on('GPS',onGPS);
+ Bangle.setGPSPower(1);
+ var n = settings.file.toString(36);
+ gpsTrack = require("Storage").open(".gpsrc"+n,"a");
+ } else {
+ WIDGETS["gpsrec"].width = 0;
+ Bangle.setGPSPower(0);
+ gpsTrack = undefined;
+ }
+ }
+ // add the widget
+ WIDGETS["gpsrec"]={area:"tl",width:24,draw:draw,reload:function() {
+ reload();
+ Bangle.drawWidgets(); // relayout all widgets
+ }};
+ // load settings, set correct widget width
+ reload();
+})()
diff --git a/apps/gpstime/ChangeLog b/apps/gpstime/ChangeLog
new file mode 100644
index 000000000..e91b36f52
--- /dev/null
+++ b/apps/gpstime/ChangeLog
@@ -0,0 +1 @@
+0.03: Fix time output on new firmwares when no GPS time set (fix #104)
diff --git a/apps/gpstime/gpstime.js b/apps/gpstime/gpstime.js
index 285fb64ba..97487ac85 100644
--- a/apps/gpstime/gpstime.js
+++ b/apps/gpstime/gpstime.js
@@ -5,11 +5,11 @@ Bangle.setLCDTimeout(0);
g.clear();
-
-
var fix;
+Bangle.setGPSPower(1);
Bangle.on('GPS',function(f) {
fix = f;
+ g.reset(1);
g.setFont("6x8",2);
g.setFontAlign(0,0);
g.clearRect(90,30,239,90);
@@ -22,9 +22,12 @@ Bangle.on('GPS',function(f) {
}
g.setFont("6x8");
g.drawString(fix.satellites+" satellites",170,80);
-
+
g.clearRect(0,100,239,239);
- var t = fix.time.toString().split(" ");/*
+ var t = ["","","","---",""];
+ if (fix.time!==undefined)
+ t = fix.time.toString().split(" ");
+ /*
[
"Sun",
"Nov",
@@ -42,23 +45,24 @@ Bangle.on('GPS',function(f) {
g.drawString(t[3],120,160); // year
g.setFont("6x8",3);
g.drawString(t[4],120,185); // time
- // timezone
- var tz = (new Date()).getTimezoneOffset()/60;
- if (tz==0) tz="UTC";
- else if (tz>0) tz="UTC+"+tz;
- else tz="UTC"+tz;
- g.setFont("6x8",2);
- g.drawString(tz,120,210); // gmt
- g.setFontAlign(0,0,3);
- g.drawString("Set",230,120);
- g.setFontAlign(0,0);
+ if (fix.time) {
+ // timezone
+ var tz = (new Date()).getTimezoneOffset()/60;
+ if (tz==0) tz="UTC";
+ else if (tz>0) tz="UTC+"+tz;
+ else tz="UTC"+tz;
+ g.setFont("6x8",2);
+ g.drawString(tz,120,210); // gmt
+ g.setFontAlign(0,0,3);
+ g.drawString("Set",230,120);
+ g.setFontAlign(0,0);
+ }
});
setInterval(function() {
g.drawImage(img,48,48,{scale:1.5,rotate:Math.sin(getTime()*2)/2});
},100);
setWatch(function() {
- setTime(fix.time.getTime()/1000);
+ if (fix.time!==undefined)
+ setTime(fix.time.getTime()/1000);
}, BTN2, {repeat:true});
-
-Bangle.setGPSPower(1)
diff --git a/apps/gpstime/gpstime.json b/apps/gpstime/gpstime.json
deleted file mode 100644
index 813914e08..000000000
--- a/apps/gpstime/gpstime.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"GPS Time","type":"app",
- "icon":"*gpstime",
- "src":"-gpstime"
-}
diff --git a/apps/grocery/grocery-icon.js b/apps/grocery/grocery-icon.js
new file mode 100644
index 000000000..949b0e45b
--- /dev/null
+++ b/apps/grocery/grocery-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/AFEEolAC6lN7vdDCcECwPd6guVGCYuDC4cCBQMikQXQJAMjkECmcyIx4XDmUjmYvLC4XUDARHBIoIWLgATCGQdA7tEonQC5ouDDYg0BOxgSEAggwKRwgUCC6ZIDSwoXNogWDDgNCAgIWIkUEoUk6kiCgMkokipsiBIQXIki2CAgNCAoYADC5Eic4Mic4ICCAIIJCC5MzAAcykYGEAAIXOABAXTmUzGoIXVAIIXLB4SICDIovjO76PZbYR3PDI4XiI6530MIh3SC6R33C/oAOC48CCxsgC44A/ADY="))
diff --git a/apps/grocery/grocery.html b/apps/grocery/grocery.html
new file mode 100644
index 000000000..12711375d
--- /dev/null
+++ b/apps/grocery/grocery.html
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
List of products
+
+
+
+
name
+
quantity
+
actions
+
+
+
+
+
+
+
+
Add a new product
+
+
+
+
+
+
+
+
+
diff --git a/apps/grocery/grocery.png b/apps/grocery/grocery.png
new file mode 100644
index 000000000..93a29a4a6
Binary files /dev/null and b/apps/grocery/grocery.png differ
diff --git a/apps/heart/ChangeLog b/apps/heart/ChangeLog
new file mode 100644
index 000000000..4c4db83bc
--- /dev/null
+++ b/apps/heart/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+
diff --git a/apps/heart/app-icon.js b/apps/heart/app-icon.js
new file mode 100644
index 000000000..a7f67291e
--- /dev/null
+++ b/apps/heart/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwwhC/AH4AWzIAByAHDhIICCpINDDAgIIFpAADBBQuKE4QIIFxgAKC7g9HABSbIBQQXWGxgXEKQxOMC5AhBC66WMC5AuBJ5h3ICoI3LeAwKBBAICBD4TmHC48ACgQCCfxC/HAgYXDL44vFA4YRDAoiOIHAgXFYRAXFBwwIIOw4OGIxKmIC5ylHGAoXIXpBIGLxxIIIx6IJFxwwNCxQwLFxYwLCxgwJFxowJCxwwHFx4wHCyAwFFyIwFCyQYDCygA/AH4AFA"))
\ No newline at end of file
diff --git a/apps/heart/app-settings.json b/apps/heart/app-settings.json
new file mode 100644
index 000000000..d57d970d0
--- /dev/null
+++ b/apps/heart/app-settings.json
@@ -0,0 +1,4 @@
+{
+ "isRecording":false,
+ "fileNbr":0
+}
diff --git a/apps/heart/app.js b/apps/heart/app.js
new file mode 100644
index 000000000..366a1068d
--- /dev/null
+++ b/apps/heart/app.js
@@ -0,0 +1,100 @@
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+
+var settings = require("Storage").readJSON("heart.json",1)||{};
+
+function getFileNbr(n) {
+ return ".heart"+n.toString(36);
+}
+
+function updateSettings() {
+ require("Storage").write("heart.json", settings);
+ if (WIDGETS["heart"])
+ WIDGETS["heart"].reload();
+}
+
+function showMainMenu() {
+ const mainMenu = {
+ '': { 'title': 'Heart Recorder' },
+ 'RECORD': {
+ value: !!settings.isRecording,
+ format: v=>v?"On":"Off",
+ onchange: v => {
+ settings.isRecording = v;
+ updateSettings();
+ }
+ },
+ 'File Number': {
+ value: settings.fileNbr|0,
+ min: 0,
+ max: 35,
+ step: 1,
+ onchange: v => {
+ settings.isRecording = false;
+ settings.fileNbr = v;
+ updateSettings();
+ }
+ },
+ 'View Records': viewRecords,
+ '< Back': ()=>{load();}
+ };
+ return E.showMenu(mainMenu);
+}
+
+function viewRecords() {
+ const menu = {
+ '': { 'title': 'Heart Records' }
+ };
+ var found = false;
+ for (var n=0;n<36;n++) {
+ var f = require("Storage").open(getFileNbr(n),"r");
+ if (f.readLine()!==undefined) {
+ menu["Record "+n] = viewRecord.bind(null,n);
+ found = true;
+ }
+ }
+ if (!found)
+ menu["No Records Found"] = function(){};
+ menu['< Back'] = showMainMenu;
+ return E.showMenu(menu);
+}
+
+function viewRecord(n) {
+ const menu = {
+ '': { 'title': 'Heart Record '+n }
+ };
+ var heartCount = 0;
+ var heartTime;
+ var f = require("Storage").open(getFileNbr(n),"r");
+ var l = f.readLine();
+ if (l!==undefined) {
+ var c = l.split(",");
+ heartTime = new Date(c[0]*1000);
+ }
+ while (l!==undefined) {
+ heartCount++;
+ // TODO: min/max/average of heart rate?
+ l = f.readLine();
+ }
+ if (heartTime)
+ menu[" "+heartTime.toString().substr(4,17)] = function(){};
+ menu[heartCount+" records"] = function(){};
+ // TODO: option to draw it? Just scan through, project using min/max
+ menu['Erase'] = function() {
+ E.showPrompt("Delete Record?").then(function(v) {
+ if (v) {
+ settings.isRecording = false;
+ updateSettings();
+ var f = require("Storage").open(getFileNbr(n),"r");
+ f.erase();
+ viewRecords();
+ } else
+ viewRecord(n);
+ });
+ };
+ menu['< Back'] = viewRecords;
+ print(menu);
+ return E.showMenu(menu);
+}
+
+showMainMenu();
diff --git a/apps/heart/app.png b/apps/heart/app.png
new file mode 100644
index 000000000..72b2805b8
Binary files /dev/null and b/apps/heart/app.png differ
diff --git a/apps/heart/interface.html b/apps/heart/interface.html
new file mode 100644
index 000000000..4a21d2e27
--- /dev/null
+++ b/apps/heart/interface.html
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/heart/widget.js b/apps/heart/widget.js
new file mode 100644
index 000000000..7d46aa239
--- /dev/null
+++ b/apps/heart/widget.js
@@ -0,0 +1,50 @@
+(() => {
+ var settings = {};
+ var hrmToggle = true; // toggles once for each reading
+ var recFile; // file for heart rate recording
+
+ // draw your widget
+ function draw() {
+ if (!settings.isRecording) return;
+ g.reset();
+ g.setFontAlign(0,0);
+ g.clearRect(this.x,this.y,this.x+23,this.y+23);
+ g.setColor(hrmToggle?"#ff0000":"#ff8000");
+ g.fillCircle(this.x+6,this.y+6,4); // draw heart left circle
+ g.fillCircle(this.x+16,this.y+6,4); // draw heart right circle
+ g.fillPoly([this.x+2,this.y+8,this.x+20,this.y+8,this.x+11,this.y+18]); // draw heart bottom triangle
+ g.setColor(-1); // change color back to be nice to other apps
+ }
+
+ function onHRM(hrm) {
+ hrmToggle = !hrmToggle;
+ WIDGETS["heart"].draw();
+ if (recFile) recFile.write([getTime().toFixed(0),hrm.bpm,hrm.confidence].join(",")+"\n");
+ }
+
+ // Called by the heart app to reload settings and decide what's
+ function reload() {
+ settings = require("Storage").readJSON("heart.json",1)||{};
+ settings.fileNbr |= 0;
+
+ Bangle.removeListener('HRM',onHRM);
+ if (settings.isRecording) {
+ WIDGETS["heart"].width = 24;
+ Bangle.on('HRM',onHRM);
+ Bangle.setHRMPower(1);
+ var n = settings.fileNbr.toString(36);
+ recFile = require("Storage").open(".heart"+n,"a");
+ } else {
+ WIDGETS["heart"].width = 0;
+ Bangle.setHRMPower(0);
+ recFile = undefined;
+ }
+ }
+ // add the widget
+ WIDGETS["heart"]={area:"tl",width:24,draw:draw,reload:function() {
+ reload();
+ Bangle.drawWidgets(); // relayout all widgets
+ }};
+ // load settings, set correct widget width
+ reload();
+})()
diff --git a/apps/hidbkbd/hid-binary-keyboard.js b/apps/hidbkbd/hid-binary-keyboard.js
index 6c595ff8e..fa1017714 100644
--- a/apps/hidbkbd/hid-binary-keyboard.js
+++ b/apps/hidbkbd/hid-binary-keyboard.js
@@ -5,7 +5,7 @@ the touchscreen
var storage = require('Storage');
-const settings = storage.readJSON('@setting') || { HID: false };
+const settings = storage.readJSON('setting.json',1) || { HID: false };
const KEY = {
A : 4 ,
B : 5 ,
diff --git a/apps/hidbkbd/hid-binary-keyboard.json b/apps/hidbkbd/hid-binary-keyboard.json
deleted file mode 100644
index e2059a9c2..000000000
--- a/apps/hidbkbd/hid-binary-keyboard.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "Binary Keyboard","type":"app",
- "icon": "*hidbkbd",
- "src": "-hidbkbd"
-}
diff --git a/apps/hidkbd/hid-keyboard.js b/apps/hidkbd/hid-keyboard.js
index 1e734edc5..ed406e093 100644
--- a/apps/hidkbd/hid-keyboard.js
+++ b/apps/hidkbd/hid-keyboard.js
@@ -1,6 +1,6 @@
var storage = require('Storage');
-const settings = storage.readJSON('@setting') || { HID: false };
+const settings = storage.readJSON('setting.json',1) || { HID: false };
var sendHid, next, prev, toggle, up, down, profile;
@@ -49,23 +49,20 @@ function drawApp() {
}
if (next) {
-
- if (settings.HIDGestures) {
- Bangle.on('aiGesture', (v) => {
- switch (v) {
- case 'swipeleft':
- E.showMessage('next');
- setTimeout(drawApp, 1000);
- next(() => {});
- break;
- case 'swiperight':
- E.showMessage('prev');
- setTimeout(drawApp, 1000);
- prev(() => {});
- break;
- }
- });
- }
+ Bangle.on('aiGesture', (v) => {
+ switch (v) {
+ case 'swipeleft':
+ E.showMessage('next');
+ setTimeout(drawApp, 1000);
+ next(() => {});
+ break;
+ case 'swiperight':
+ E.showMessage('prev');
+ setTimeout(drawApp, 1000);
+ prev(() => {});
+ break;
+ }
+ });
setWatch(function(e) {
var len = e.time - e.lastTime;
diff --git a/apps/hidkbd/hid-keyboard.json b/apps/hidkbd/hid-keyboard.json
deleted file mode 100644
index 11653b803..000000000
--- a/apps/hidkbd/hid-keyboard.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "Keyboard Control","type":"app",
- "icon": "*hidkbd",
- "src": "-hidkbd"
-}
diff --git a/apps/hidkbd/hid-keyboard.min.js b/apps/hidkbd/hid-keyboard.min.js
deleted file mode 100644
index 5110be4d3..000000000
--- a/apps/hidkbd/hid-keyboard.min.js
+++ /dev/null
@@ -1 +0,0 @@
-function drawApp(){function b(a){return{width:8,height:a.length,bpp:1,buffer:new Uint8Array(a).buffer};}g.clear(),g.setFont('6x8',2),g.setFontAlign(0,0),g.drawString(profile,120,120);const a=g.getWidth()-18;g.drawImage(b([16,56,124,254,16,16,16,16]),a,40),g.drawImage(b([16,16,16,16,254,124,56,16]),a,194),g.drawImage(b([0,8,12,14,255,14,12,8]),a,116);}var storage=require('Storage');const settings=storage.readJSON('@setting')||{HID:!1};var sendHid,next,prev,toggle,up,down,profile;settings.HID?(profile='Keyboard',sendHid=function(b,a){try{NRF.sendHIDReport([2,0,0,b,0,0,0,0,0],()=>NRF.sendHIDReport([2,0,0,0,0,0,0,0,0],()=>a&&a();););}catch(a){print(a);}},next=function(a){sendHid(79,a);},prev=function(a){sendHid(80,a);},toggle=function(a){sendHid(44,a);},up=function(a){sendHid(82,a);},down=function(a){sendHid(81,a);}):(E.showMessage('HID disabled'),setTimeout(load,1000)),next&&(settings.HIDGestures&&Bangle.on('aiGesture',v=>switch(v){case'swipeleft':E.showMessage('next');setTimeout(drawApp,1000);next(()=>;);break;case'swiperight':E.showMessage('prev');setTimeout(drawApp,1000);prev(()=>;);break;}),setWatch(function(b){var a=b.time-b.lastTime;a>0.3&&a<0.9?(E.showMessage('prev'),setTimeout(drawApp,1000),prev(()=>;)):(E.showMessage('up'),setTimeout(drawApp,1000),up(()=>;));},BTN1,{edge:'falling',repeat:!0,debounce:50}),setWatch(function(b){var a=b.time-b.lastTime;a>0.3&&a<0.9?(E.showMessage('next'),setTimeout(drawApp,1000),next(()=>;)):(E.showMessage('down'),setTimeout(drawApp,1000),down(()=>;));},BTN3,{edge:'falling',repeat:!0,debounce:50}),setWatch(function(a){E.showMessage('toggle'),setTimeout(drawApp,1000),toggle();},BTN2,{edge:'falling',repeat:!0,debounce:50}),drawApp());
\ No newline at end of file
diff --git a/apps/hidmsic/hid-music.js b/apps/hidmsic/hid-music.js
index d4dbd5f45..034bbd231 100644
--- a/apps/hidmsic/hid-music.js
+++ b/apps/hidmsic/hid-music.js
@@ -1,6 +1,6 @@
var storage = require('Storage');
-const settings = storage.readJSON('@setting') || { HID: false };
+const settings = storage.readJSON('setting.json',1) || { HID: false };
var sendHid, next, prev, toggle, up, down, profile;
diff --git a/apps/hidmsic/hid-music.json b/apps/hidmsic/hid-music.json
deleted file mode 100644
index 419ed196e..000000000
--- a/apps/hidmsic/hid-music.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name": "Music Control","type":"app",
- "icon": "*hidmsic",
- "src": "-hidmsic"
-}
diff --git a/apps/horsey/horse-race.json b/apps/horsey/horse-race.json
deleted file mode 100644
index cf276e2ac..000000000
--- a/apps/horsey/horse-race.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Horse Race",
- "icon": "*horsey",
- "src":"-horsey"
-}
diff --git a/apps/hrings/hypno-rings.json b/apps/hrings/hypno-rings.json
deleted file mode 100644
index a21a84561..000000000
--- a/apps/hrings/hypno-rings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Hypno Rings","type":"app",
- "icon":"*hrings",
- "src":"-hrings"
-}
\ No newline at end of file
diff --git a/apps/hrm/heartrate.json b/apps/hrm/heartrate.json
deleted file mode 100644
index 72b86993e..000000000
--- a/apps/hrm/heartrate.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Heart Rate","type":"app",
- "icon":"*hrm",
- "src":"-hrm"
-}
diff --git a/apps/jbells/jbells.json b/apps/jbells/jbells.json
deleted file mode 100644
index 20638a2d6..000000000
--- a/apps/jbells/jbells.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Jingle Bells","type":"app",
- "icon":"*jbells",
- "src":"-jbells"
-}
\ No newline at end of file
diff --git a/apps/launch/app.js b/apps/launch/app.js
index 1b98ba835..682122f82 100644
--- a/apps/launch/app.js
+++ b/apps/launch/app.js
@@ -1,8 +1,5 @@
var s = require("Storage");
-var apps = s.list().filter(a=>a[0]=='+').map(app=>{
- try { return s.readJSON(app); }
- catch (e) { return {name:"DEAD: "+app.substr(1)} }
-}).filter(app=>app.type=="app" || app.type=="clock" || !app.type);
+var apps = s.list(/\.info$/).map(app=>s.readJSON(app,1)||{name:"DEAD: "+app.substr(1)}).filter(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
diff --git a/apps/launch/app.json b/apps/launch/app.json
deleted file mode 100644
index 20a78cd1d..000000000
--- a/apps/launch/app.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name":"Launcher","type":"launch",
- "src":"-launch"
-}
diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog
new file mode 100644
index 000000000..d46bbaea0
--- /dev/null
+++ b/apps/locale/ChangeLog
@@ -0,0 +1,6 @@
+0.01: New App!
+0.02: Fix locale.currencySym
+0.03: Fix global 'locale' variable
+0.04: Add function meridian
+0.05: Inline locale details - faster, less memory overhead
+ Add correct scaling for speed/distance/temperature
diff --git a/apps/locale/locale.html b/apps/locale/locale.html
new file mode 100644
index 000000000..5cb4b4598
--- /dev/null
+++ b/apps/locale/locale.html
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
Please choose a language from the following list:
+
+
+
+
Then click
+
+
+
+
+
+
+
diff --git a/apps/locale/locale.png b/apps/locale/locale.png
new file mode 100644
index 000000000..1719f6181
Binary files /dev/null and b/apps/locale/locale.png differ
diff --git a/apps/locale/locales.js b/apps/locale/locales.js
new file mode 100644
index 000000000..43e073cd1
--- /dev/null
+++ b/apps/locale/locales.js
@@ -0,0 +1,407 @@
+/* jshint esversion: 6 */
+const distanceUnits = { // how many meters per X?
+ "m" : 1,
+ "yd" : 0.9144,
+ "mi" : 1609.34,
+ "km" : 1000,
+ "kmi" : 1000
+};
+const speedUnits = { // how many kph per X?
+ "kmh" : 1,
+ "kph" : 1,
+ "mph" : 1.60934
+};
+
+/*
+timePattern / datePattern:
+
+ %Y year four digits
+ %y last two digits of year (00..99)
+ %m month (01..12)
+ %d day of month (e.g, 01)
+
+ %a locale's abbreviated weekday name (e.g., Sun)
+ %A locale's full weekday name (e.g., Sunday)
+ %b locale's abbreviated month name (e.g., Jan)
+ %B locale's full month name (e.g., January)
+
+ %H hour (00..23)
+ %M minute (00..59)
+ %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
+ lang: "en_GB",
+ decimal_point: ".",
+ thousands_sep: ",",
+ currency_symbol: "£", currency_first:true,
+ int_curr_symbol: "GBP",
+ speed: 'mph',
+ distance: { "0": "yd", "1": "mi" },
+ 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",
+ trans: { /*yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off"*/ }},
+ "de_DE": {
+ lang: "de_DE",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "\x80",
+ 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: "%A, %d. %B %Y", "1": "%d.%m.%Y" }, // Sonntag, 1. März 2020 // 01.01.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" }},
+ "en_US": {
+ lang: "en_US",
+ decimal_point: ".",
+ thousands_sep: ",",
+ currency_symbol: "$", currency_first:true,
+ int_curr_symbol: "USD",
+ speed: "mph",
+ distance: { 0: "yd", 1: "mi" },
+ temperature: "°F",
+ ampm: {0:"am",1:"pm"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "", 1: "%m/%d/%y" },
+ 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",
+ trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }},
+ "en_JP": { // we do not have the font, so it is not ja_JP
+ lang: "en_JP",
+ decimal_point: ".",
+ thousands_sep: ",",
+ currency_symbol: "¥",
+ int_curr_symbol: "JPY",
+ speed: "kmh",
+ distance: { 0: "m", 1: "km" },
+ temperature: "°F",
+ ampm: {0:"",1:""},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "%y/%M/%d", 1: "%y/%m;/%d" },
+ 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",
+ trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }},
+ "nl_NL": {
+ lang: "nl_NL",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "\x80",
+ 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: "%A %B %d %Y", 1: "%d.%m.%y" }, // zondag 1 maart 2020 // 01.01.20
+ 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",
+ trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }},
+ "en_CA": {
+ lang: "en_CA",
+ decimal_point: ".",
+ thousands_sep: ",",
+ currency_symbol: "$",
+ int_curr_symbol: "CAD",
+ speed: "mph",
+ distance: { 0: "mi", 1: "kmi" },
+ temperature: "°F",
+ ampm: {0:"am",1:"pm"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "%A, %B %d, %Y", "1": "%Y-%m-%d" }, // Sunday, March 1, 2020 // 2012-12-20
+ 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",
+ trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }},
+ "fr_FR": {
+ lang: "fr_FR",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "\x80",
+ 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: "%A %d %B %Y", "1": "%d/%m/%Y" }, // dimanche 1 mars 2020 // 01/03/2020
+ abmonth: "janv,févr,mars,avril,mai,juin,juil,août,sept,oct,nov,déc",
+ month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
+ abday: "dim,lun,mar,mer,jeu,ven,sam",
+ day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi",
+ trans : { yes : "oui", Yes: "Oui", no: "no", No: "No", ok : "ok", on: "on", off: "off" }},
+ "sv_SE": {
+ lang: "sv_SE",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "kr",
+ int_curr_symbol: "SKR",
+ speed: "kmh",
+ distance: { 0: "m", 1: "km" },
+ 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
+ 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_AU": {
+ lang: "en_AU",
+ decimal_point: ".",
+ thousands_sep: ",",
+ currency_symbol: "$",
+ int_curr_symbol: "AUD",
+ speed: "mph",
+ distance: { 0: "mi", 1: "kmi" },
+ temperature: "°F",
+ ampm: {0:"am",1:"pm"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "%A, %B %d, %Y", "1": "%m/%d/%y" }, // Sunday, 1 March 2020 // 1/3/20
+ 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",
+ trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }},
+ "de_AT": {
+ lang: "de_AT",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "\x80",
+ int_curr_symbol: "EUR",
+ speed: "kmh",
+ distance: { 0: "m", 1: "km" },
+ temperature: "°C",
+ 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" }},
+ "en_IL": {
+ lang: "en_IL",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "₪",
+ int_curr_symbol: "ILS",
+ speed: "kmh",
+ distance: { 0: "m", 1: "km" },
+ temperature: "°F",
+ ampm: {0:"am",1:"pm"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "%A, %B %d, %Y", "1": "%d/%m/%Y" }, // Sunday, 1 March 2020 // 01/03/2020
+ 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",
+ trans: { yes: "yes", Yes: "Yes", no: "no", No: "No", ok: "ok", on: "on", off: "off" }},
+ "es_ES": {
+ lang: "es_ES",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "\x80",
+ 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: "%A, %d de %B de %Y", "1": "%d/%m/%y" }, // domingo, 1 de marzo de 2020 // 01/03/20
+ abmonth: "ene,feb,mar,abr,may,jun,jul,ago,sept,oct,nov,dic",
+ 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" }},
+ "fr_BE": {
+ lang: "fr_BE",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "\x80",
+ 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: "%A %B %d %Y", "1": "%d/%m/%y" }, // dimanche 1 mars 2020 // 01/03/20
+ abmonth: "anv.,févr.,mars,avril,mai,juin,juil.,août,sept.,oct.,nov.,déc.",
+ month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
+ abday: "dim,lun,mar,mer,jeu,ven,sam",
+ day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi",
+ trans : { yes : "oui", Yes: "Oui", no: "no", No: "No", ok : "ok", on: "on", off: "off" }},
+ "fi_FI": {
+ lang: "fi_FI",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "\x80",
+ int_curr_symbol: "EUR",
+ speed: "kmh",
+ distance: { 0: "m", 1: "km" },
+ temperature: "°C",
+ ampm: {0: "ap",1: "ip"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" }, // 17.00.00 // 17.00
+ datePattern: { 0: "%A %d. %B %Y", "1": "%-d/%-m/%Y" }, // sunnuntai 1. maaliskuuta 2020 // 1.3.2020
+ abmonth: "tammik,helmik,maalisk,huhtik,toukok,kesäk,heinäk,elok,syysk,lokak,marrask,jouluk",
+ month: "tammikuuta,helmikuuta,maaliskuuta,huhtikuuta,toukokuuta,kesäkuuta,heinäkuuta,elokuuta,syyskuuta,lokakuuta,marraskuuta,joulukuuta",
+ abday: "su,ma,ti,ke,to,pe,la",
+ day: "sunnuntaina,maanantaina,tiistaina,keskiviikkona,torstaina,perjantaina,lauantaina",
+ trans : { yes : "oui", Yes: "Oui", no: "no", No: "No", ok : "ok", on: "on", off: "off" }},
+ "de_CH": {
+ lang: "de_CH",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "CHF",
+ int_curr_symbol: "CHF",
+ speed: "kmh",
+ distance: { 0: "m", 1: "km" },
+ temperature: "°C",
+ ampm: {0:"vorm",1:" nachm"},
+ timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
+ datePattern: { 0: "%A, %d. %B %Y", "1": "%d.%m.%Y" }, // Sonntag, 1. März 2020 // 1.3.2020
+ abmonth: "Jan,Feb,März,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" }},
+ "fr_CH": {
+ lang: "fr_CH",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "CHF",
+ int_curr_symbol: "CHF",
+ speed: "kmh",
+ distance: { 0: "m", 1: "km" },
+ temperature: "°C",
+ ampm: {0:"AM",1:"PM"},
+ timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
+ datePattern: { 0: "%A %d %B %Y", "1": "%d/%m/%y" }, // dimanche 1 mars 2020 // 01/03/20
+ abmonth: "anv.,févr.,mars,avril,mai,juin,juil.,août,sept.,oct.,nov.,déc.",
+ month: "janvier,février,mars,avril,mai,juin,juillet,août,septembre,octobre,novembre,décembre",
+ abday: "dim,lun,mar,mer,jeu,ven,sam",
+ day: "dimanche,lundi,mardi,mercredi,jeudi,vendredi,samedi",
+ trans : { yes : "oui", Yes: "Oui", no: "no", No: "No", ok : "ok", on: "on", off: "off" }},
+ "it_CH": {
+ lang: "it_CH",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "CHF",
+ int_curr_symbol: "CHF",
+ speed: 'kmh',
+ distance: { "0": "m", "1": "km" },
+ temperature: '°C',
+ 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
+ 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",
+ trans : { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" }},
+ "wae_CH" : {
+ lang: "wae_CH",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "CHF",
+ int_curr_symbol: "CHF",
+ speed: 'kmh',
+ distance: { "0": "m", "1": "km" },
+ temperature: '°C',
+ timePattern: { 0: "%HH.%MM.%SS ", 1: "%HH.%MM" }, // 17.00.00 // 17.00
+ datePattern: { 0: "%A, %d. %B %Y", "1": "%Y-%m-%d" }, // Sunntag, 1. Märze 2020 // 2020-03-01
+ abmonth: "Jen,Hor,Mär,Abr,Mei,Brá,Hei,Öig,Her,Wím,Win,Chr",
+ month: "Jenner,Hornig,Märze,Abrille,Meije,Bráčet,Heiwet,Öigšte,Herbštmánet,Wímánet,Wintermánet,Chrištmánet",
+ abday: "Sun,Män,Ziš,Mit,Fró,Fri,Sam",
+ day: "Sunntag,Mäntag,Zištag,Mittwuč,Fróntag,Fritag,Samštag",
+ trans : { yes: "sì", Yes: "Sì", no: "no", No: "No", ok: "ok", on: "on", off: "off" }},
+ "tr_TR": { // this is default
+ lang: "tr_TR",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "TL",
+ int_curr_symbol: "TRY",
+ speed: 'kmh',
+ distance: { "0": "m", "1": "km" },
+ temperature: '°C',
+ ampm: {0:"öö",1:"ös"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "%d %w %Y %A", 1: "%d/%m/%Y" }, // 1 Mart 2020 Pazar // "01/03/2020"
+ abmonth: "Oca,Sub,Mar,Nis,May,Haz,Tem,Agu,Eyl,Eki,Kas,Ara",
+ month: "Ocak,Subat,Mart,Nisan,Mayis,Haziran,Temmuz,Agustos,Eylul,Ekim,Kasim,Aralik",
+ abday: "Paz,Pzt,Sal,Car,Per,Cum,Cmt",
+ day: "Pazar,Pazartesi,Sali,Carsamba,Persembe,Cuma,Cumartesi",
+ trans: { yes: "evet", Yes: "Evet", no: "hayir", No: "Hayir", ok: "tamam", on: "acik", off: "kapali" }},
+ "hu_HU": {
+ lang: "hu_HU",
+ decimal_point: ",",
+ thousands_sep: " ",
+ currency_symbol: "Ft",
+ int_curr_symbol: "HUF",
+ speed: 'kph',
+ distance: { "0": "m", "1": "km" },
+ temperature: '°C',
+ ampm: {0:"de",1:"du"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "%Y %b %d, %A", 1: "%Y.%m.%d" }, // 2020 Feb 28, Péntek" // "2020.03.01."(short)
+ abmonth: "Jan,Feb,Már,Ápr,Máj,Jún,Júl,Aug,Szep,Okt,Nov,Dec",
+ month: "Január,Február,Március,Április,Május,Június,Július,Augusztus,Szeptember,Október,November,December",
+ abday: "Vas,Hét,Ke,Szer,Csüt,Pén,Szom",
+ day: "Vasárnap,Hétfő,Kedd,Szerda,Csütörtök,Péntek,Szombat",
+ trans: { yes: "igen", Yes: "Igen", no: "nem", No: "Nem", ok: "ok", on: "be", off: "ki" }},
+ "pt_BR": {
+ lang: "pt_BR",
+ decimal_point: ",",
+ thousands_sep: ".",
+ currency_symbol: "R$", currency_first:true,
+ int_curr_symbol: "BRL",
+ speed: "kmh",
+ distance: { 0: "m", 1: "km" },
+ temperature: "°C",
+ ampm: {0:"am",1:"pm"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "", 1: "%d/%m/%y" },
+ abmonth: "Jan,Fev,Mar,Abr,Mai,Jun,Jul,Ago,Set,Out,Nov,Dez",
+ month: "Janeiro,Fevereiro,Março,Abril,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro",
+ abday: "Dom,Seg,Ter,Qua,Qui,Sex,Sab",
+ 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: "certo", on: "ligado", off: "desligado" }},
+ "cs_CZ": {
+ lang: "cs_CZ",
+ decimal_point: ",",
+ thousands_sep: " ",
+ currency_symbol: "Kč",
+ int_curr_symbol: " CZK",
+ speed: 'kmh',
+ distance: { "0": "m", "1": "km" },
+ temperature: '°C',
+ ampm: {0:"dop",1:"odp"},
+ timePattern: { 0: "%HH:%MM:%SS ", 1: "%HH:%MM" },
+ datePattern: { 0: "%d. %b %Y", 1: "%d.%m.%Y" }, // "30. led 2020" // "30.01.2020"(short)
+ abmonth: "led,úno,bře,dub,kvě,čvn,čvc,srp,zář,říj,lis,pro",
+ 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" }}
+};
diff --git a/apps/marioclock/ChangeLog b/apps/marioclock/ChangeLog
new file mode 100644
index 000000000..c16d02fc8
--- /dev/null
+++ b/apps/marioclock/ChangeLog
@@ -0,0 +1,7 @@
+0.01: Create mario app [Paul Cockrell https://github.com/paulcockrell]
+0.02: Fix day of the week and add padding
+0.03: use short date format from locale, take timeout from settings
+0.04: modify date to display to be more at the original idea but still localized
+0.05: use 12/24 hour clock from settings
+0.06: Performance refactor, and enhanced graphics!
+0.07: Swipe right to change between Mario and Toad characters, swipe left to toggle night mode
\ No newline at end of file
diff --git a/apps/marioclock/README.md b/apps/marioclock/README.md
new file mode 100644
index 000000000..a74c863f8
--- /dev/null
+++ b/apps/marioclock/README.md
@@ -0,0 +1,19 @@
+# Mario Clock
+Let's go back in time with this Gameboy inspired Mario retro clock.
+
+Enjoy watching Mario, or one of the other game characters run through a level while showing you the time and date.
+
+
+
+## Features
+
+* Multiple characters - swipe the screen right to change the character
+* Night and Day modes - swipe left to toggle mode
+* Smooth animation
+* Awesome 8-bit style grey-scale graphics
+* Mario jumps to change the time, every minute
+* You can make Mario jump by pressing the top button (Button 1) on the watch
+
+## Requests
+
+If you have any feature requests, please send an email to the author `paulcockrell@gmail.com`
diff --git a/apps/marioclock/mario-clock-screen-shot.png b/apps/marioclock/mario-clock-screen-shot.png
new file mode 100755
index 000000000..ae2dc7800
Binary files /dev/null and b/apps/marioclock/mario-clock-screen-shot.png differ
diff --git a/apps/marioclock/marioclock-app.js b/apps/marioclock/marioclock-app.js
new file mode 100644
index 000000000..dabe7ad9e
--- /dev/null
+++ b/apps/marioclock/marioclock-app.js
@@ -0,0 +1,399 @@
+/**
+ * BangleJS MARIO CLOCK
+ *
+ * + Original Author: Paul Cockrell https://github.com/paulcockrell
+ * + Created: April 2020
+ * + Based on Espruino Mario Clock V3 https://github.com/paulcockrell/espruino-mario-clock
+ * + Online Image convertor: https://www.espruino.com/Image+Converter, Use transparency + compression + 8bit Web + export as Image String
+ * + Images must be drawn as PNGs with transparent backgrounds
+ */
+
+const locale = require("locale");
+const storage = require('Storage');
+const settings = (storage.readJSON('setting.json', 1) || {});
+const timeout = settings.timeout || 10;
+const is12Hour = settings["12hour"] || false;
+
+// Screen dimensions
+let W, H;
+
+let intervalRef, displayTimeoutRef = null;
+
+// Colours
+const LIGHTEST = "#effedd";
+const LIGHT = "#add795";
+const DARK = "#588d77";
+const DARKEST = "#122d3e";
+const NIGHT = "#001818";
+
+// Character names
+const TOAD = "toad";
+const MARIO = "mario";
+
+const characterSprite = {
+ frameIdx: 0,
+ x: 35,
+ y: 55,
+ jumpCounter: 0,
+ jumpIncrement: Math.PI / 6,
+ isJumping: false,
+ character: MARIO,
+};
+
+const coinSprite = {
+ frameIdx: 0,
+ x: 34,
+ y: 18,
+ isAnimating: false,
+ yDefault: 18,
+};
+
+const pyramidSprite = {
+ x: 90,
+ height: 34,
+};
+
+const ONE_SECOND = 1000;
+
+let timer = 0;
+let backgroundArr = [];
+let nightMode = false;
+
+function genRanNum(min, max) {
+ return Math.floor(Math.random() * (max - min + 1) + min);
+}
+
+function switchCharacter() {
+ const curChar = characterSprite.character;
+ let newChar;
+ if (curChar === MARIO) {
+ newChar = TOAD;
+ } else {
+ newChar = MARIO;
+ }
+
+ characterSprite.character = newChar;
+}
+
+function toggleNightMode() {
+ nightMode = !nightMode;
+}
+
+function incrementTimer() {
+ if (timer > 1000) {
+ timer = 0;
+ }
+ else {
+ timer += 50;
+ }
+}
+
+function drawBackground() {
+ // Clear screen
+ const bgColor = (nightMode) ? NIGHT : LIGHTEST;
+ g.setColor(bgColor);
+ g.fillRect(0, 10, W, H);
+
+ // set cloud colors and draw clouds
+ const cloudColor = (nightMode) ? DARK : LIGHT;
+ g.setColor(cloudColor);
+ g.fillRect(0, 10, g.getWidth(), 15);
+ g.fillRect(0, 17, g.getWidth(), 17);
+ g.fillRect(0, 19, g.getWidth(), 19);
+ g.fillRect(0, 21, g.getWidth(), 21);
+
+ // Date bar
+ g.setColor(DARKEST);
+ g.fillRect(0, 0, W, 9);
+}
+
+function drawFloor() {
+ const fImg = require("heatshrink").decompress(atob("ikDxH+rgATCoIBQAQYDP")); // Floor image
+ for (let x = 0; x < 4; x++) {
+ g.drawImage(fImg, x * 20, g.getHeight() - 5);
+ }
+}
+
+function drawPyramid() {
+ const pPol = [pyramidSprite.x + 10, H - 6, pyramidSprite.x + 50, pyramidSprite.height, pyramidSprite.x + 90, H - 6]; // Pyramid poly
+
+ const color = (nightMode) ? DARK : LIGHT;
+ g.setColor(color);
+ g.fillPoly(pPol);
+
+ pyramidSprite.x -= 1;
+ // Reset and randomize pyramid if off-screen
+ if (pyramidSprite.x < - 100) {
+ pyramidSprite.x = 90;
+ pyramidSprite.height = genRanNum(25, 60);
+ }
+}
+
+function drawTreesFrame(x, y) {
+ const tImg = require("heatshrink").decompress(atob("h8GxH+AAMHAAIFCAxADEBYgDCAQYAFCwobOAZAEFBxo=")); // Tree image
+
+ g.drawImage(tImg, x, y);
+ g.setColor(DARKEST);
+ g.drawLine(x + 6 /* Match stalk to palm tree */, y + 6 /* Match stalk to palm tree */, x + 6, H - 6);
+}
+
+function generateTreeSprite() {
+ return {
+ x: 90,
+ y: genRanNum(30, 60)
+ };
+}
+
+function drawTrees() {
+ // remove first sprite if offscreen
+ let firstBackgroundSprite = backgroundArr[0];
+ if (firstBackgroundSprite) {
+ if (firstBackgroundSprite.x < -15) backgroundArr.splice(0, 1);
+ }
+
+ // set background sprite if array empty
+ let lastBackgroundSprite = backgroundArr[backgroundArr.length - 1];
+ if (!lastBackgroundSprite) {
+ const newSprite = generateTreeSprite();
+ lastBackgroundSprite = newSprite;
+ backgroundArr.push(lastBackgroundSprite);
+ }
+
+ // add random sprites
+ if (backgroundArr.length < 2 && lastBackgroundSprite.x < (16 * 7)) {
+ const randIdx = Math.floor(Math.random() * 25);
+ if (randIdx < 2) {
+ const newSprite = generateTreeSprite();
+ backgroundArr.push(newSprite);
+ }
+ }
+
+ for (x = 0; x < backgroundArr.length; x++) {
+ let scenerySprite = backgroundArr[x];
+ scenerySprite.x -= 5;
+ drawTreesFrame(scenerySprite.x, scenerySprite.y);
+ }
+}
+
+function drawCoinFrame(x, y) {
+ const cImg = require("heatshrink").decompress(atob("hkPxH+AAcHAAQIEBIXWAAQNEBIWHAAdcBgQLBA4IODBYQKEBAQMDBelcBaJUBM4QRBNYx1EBQILDR4QHBBISdIBIoA==")); // Coin image
+ g.drawImage(cImg, x, y);
+}
+
+function drawCoin() {
+ if (!coinSprite.isAnimating) return;
+
+ coinSprite.y -= 8;
+ if (coinSprite.y < (0 - 15 /*Coin sprite height*/)) {
+ coinSprite.isAnimating = false;
+ coinSprite.y = coinSprite.yDefault;
+ return;
+ }
+
+ drawCoinFrame(coinSprite.x, coinSprite.y);
+}
+
+function drawMarioFrame(idx, x, y) {
+ switch(idx) {
+ case 0:
+ const mFr1 = require("heatshrink").decompress(atob("h8UxH+AAkHAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw52FSg2HAAIoDAgIOMB5AAFGQTtKeBLuNcQwOJFwgJFA=")); // Mario Frame 1
+ g.drawImage(mFr1, x, y);
+ break;
+ case 1:
+ const mFr2 = require("heatshrink").decompress(atob("h8UxH+AAkHAAYKFBolcAAIPIBgYPDBpgfGFIY7EA4YcEBIPWAAYdDC4gLDAII5ECoYOFDogODFgoJCBwYZCAQYOFBAhAFFwZKGHQpMDw+HCQYEBSowOBBQIdCCgTOIFgiVHFwYCBUhA9FBwz8HAo73GACQA=")); // Mario frame 2
+ g.drawImage(mFr2, x, y);
+ break;
+ default:
+ }
+}
+
+function drawToadFrame(idx, x, y) {
+ switch(idx) {
+ case 0:
+ const tFr1 = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYYrCCAwbFFwgQEM4gAEeA4OIH4ghFAAYLD")); // Toad Frame 1
+ g.drawImage(tFr1, x, y);
+ break;
+ case 1:
+ const tFr2 = require("heatshrink").decompress(atob("iEUxH+ACkHAAoNJrnWAAQRGg/WrgACB4QEBCAYOBB44QFB4QICAg4QBBAQbDEgwPCHpAGCGAQ9KAYQPKCYg/EJAoADAwaKFw4BEP4YQCBIIABB468EB4QADYIoQGDwQOGBYQrDb4wcGFxYLDMoYgHRYgwKABAMBA")); // Mario frame 2
+ g.drawImage(tFr2, x, y);
+ break;
+ default:
+ }
+}
+
+function drawCharacter(date, character) {
+ // calculate jumping
+ const seconds = date.getSeconds(),
+ milliseconds = date.getMilliseconds();
+
+ if (seconds == 59 && milliseconds > 800 && !characterSprite.isJumping) {
+ characterSprite.isJumping = true;
+ }
+
+ if (characterSprite.isJumping) {
+ characterSprite.y = (Math.sin(characterSprite.jumpCounter) * -12) + 50 /* Character Y base value */;
+ characterSprite.jumpCounter += characterSprite.jumpIncrement;
+
+ if (parseInt(characterSprite.jumpCounter) === 2 && !coinSprite.isAnimating) {
+ coinSprite.isAnimating = true;
+ }
+
+ if (characterSprite.jumpCounter.toFixed(1) >= 4) {
+ characterSprite.jumpCounter = 0;
+ characterSprite.isJumping = false;
+ }
+ }
+
+ // calculate animation timing
+ if (timer % 50 === 0) {
+ // shift to next frame
+ characterSprite.frameIdx ^= 1;
+ }
+
+ switch(characterSprite.character) {
+ case(TOAD):
+ drawToadFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y);
+ break;
+ case(MARIO):
+ default:
+ drawMarioFrame(characterSprite.frameIdx, characterSprite.x, characterSprite.y);
+ }
+}
+
+function drawBrickFrame(x, y) {
+ const brk = require("heatshrink").decompress(atob("ikQxH+/0HACASB6wAQCoPWw4AOrgT/Cf4T/Cb1cAB8H/wVBAB/+A"));
+ g.drawImage(brk, x, y);
+}
+
+function drawTime(date) {
+ // draw hour brick
+ drawBrickFrame(20, 25);
+ // draw minute brick
+ drawBrickFrame(42, 25);
+
+ const h = date.getHours();
+ const hours = ("0" + ((is12Hour && h > 12) ? h - 12 : h)).substr(-2);
+ const mins = ("0" + date.getMinutes()).substr(-2);
+
+ g.setFont("6x8");
+ g.setColor(DARKEST);
+ g.drawString(hours, 25, 29);
+ g.drawString(mins, 47, 29);
+}
+
+function drawDate(date) {
+ g.setFont("6x8");
+ g.setColor(LIGHTEST);
+ let dateStr = locale.date(date, true);
+ dateStr = dateStr.replace(date.getFullYear(), "").trim().replace(/\/$/i,"");
+ dateStr = locale.dow(date, true) + " " + dateStr;
+ g.drawString(dateStr, (W - g.stringWidth(dateStr))/2, 1);
+}
+
+function redraw() {
+ const date = new Date();
+
+ // Update timers
+ incrementTimer();
+
+ // Draw frame
+ drawBackground();
+ drawFloor();
+ drawPyramid();
+ drawTrees();
+ drawTime(date);
+ drawDate(date);
+ drawCharacter(date);
+ drawCoin();
+
+ // Render new frame
+ g.flip();
+}
+
+function clearTimers(){
+ if(intervalRef) {
+ clearInterval(intervalRef);
+ intervalRef = null;
+ }
+
+ if(displayTimeoutRef) {
+ clearInterval(displayTimeoutRef);
+ displayTimeoutRef = null;
+ }
+}
+
+function resetDisplayTimeout() {
+ if (displayTimeoutRef) clearInterval(displayTimeoutRef);
+
+ displayTimeoutRef = setInterval(() => {
+ if (Bangle.isLCDOn()) Bangle.setLCDPower(false);
+ clearTimers();
+ }, ONE_SECOND * timeout);
+}
+
+function startTimers(){
+ if(intervalRef) clearTimers();
+ intervalRef = setInterval(redraw, 50);
+
+ resetDisplayTimeout();
+
+ redraw();
+}
+
+// Main
+function init() {
+ clearInterval();
+
+ // Initialise display
+ Bangle.setLCDMode("80x80");
+
+ // Store screen dimensions
+ W = g.getWidth();
+ H = g.getHeight();
+
+ // Get Mario to jump!
+ setWatch(() => {
+ if (intervalRef && !characterSprite.isJumping) characterSprite.isJumping = true;
+ resetDisplayTimeout();
+ }, BTN1, {repeat:true});
+
+ setWatch(() => {
+ Bangle.setLCDMode();
+ Bangle.showLauncher();
+ }, BTN2, {repeat:false,edge:"falling"});
+
+ Bangle.on('lcdPower', (on) => {
+ if (on) {
+ startTimers();
+ } else {
+ clearTimers();
+ }
+ });
+
+ Bangle.on('faceUp', (up) => {
+ if (up && !Bangle.isLCDOn()) {
+ clearTimers();
+ Bangle.setLCDPower(true);
+ }
+ });
+
+ Bangle.on('swipe', (sDir) => {
+ resetDisplayTimeout();
+
+ switch(sDir) {
+ // Swipe right (1) - change character (on a loop)
+ case(1):
+ switchCharacter();
+ break;
+ // Swipe left (-1) - change day/night mode (on a loop)
+ case(-1):
+ default:
+ toggleNightMode();
+ }
+ });
+
+ startTimers();
+}
+
+// Initialise!
+init();
\ No newline at end of file
diff --git a/apps/marioclock/marioclock-icon.js b/apps/marioclock/marioclock-icon.js
new file mode 100644
index 000000000..223d911f4
--- /dev/null
+++ b/apps/marioclock/marioclock-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AAmBAEgrFAAZelFo+s1krAEgnBFwo5BBIIAi64nBSQgvnE4QvCwMAM4SRCXsAlFqwvGdsPXF5QHBRsYvJBYXWAD4vdw4ABF9gRBF8xYGF4plLF6xYMw4NBF74SBF9ocHAATvpFwgwOF6J9IFwwNIUIgvZFw4xGF7IABFxwwECxC/WE4gkCAALBMF64uHAYLvfC4xXDF4pflF4aPFBIQvZVo5bFAQYvIDQgvYFIjEFSIwvVAALwJGQyKGDQi/XSIYyDdhjvaYBIwNF8AwOF6LBHRoqVDXpIvUWQ4wGBpAvhSQaNHF7DCNdxwvcSIgRNF659GAAqUJF/4vIXAQGJBgy/hMxov/F6QARF/es64NBAD/XEwQvCqwvBBIYvhEgQvD/3+RoYAkFoOBFoIvqE4IvE/2BwMrAEgnBFwgACXsIADFo4ABeoIAjFQgA="))
diff --git a/apps/marioclock/marioclock.png b/apps/marioclock/marioclock.png
new file mode 100644
index 000000000..a462cdea1
Binary files /dev/null and b/apps/marioclock/marioclock.png differ
diff --git a/apps/mclock/ChangeLog b/apps/mclock/ChangeLog
index 7819dbe2a..e6a689e9e 100644
--- a/apps/mclock/ChangeLog
+++ b/apps/mclock/ChangeLog
@@ -1 +1,2 @@
0.02: Modified for use with new bootloader and firmware
+0.03: Added Locale based date
diff --git a/apps/mclock/clock-morphing.js b/apps/mclock/clock-morphing.js
index 50e81ec72..ce30ad033 100644
--- a/apps/mclock/clock-morphing.js
+++ b/apps/mclock/clock-morphing.js
@@ -1,3 +1,4 @@
+var locale = require("locale");
// Offscreen buffer
var buf = Graphics.createArrayBuffer(240,86,1,{msb:true});
function flip() {
@@ -145,7 +146,7 @@ function draw(lastText,thisText,n) {
buf.drawString(("0"+d.getSeconds()).substr(-2), x, y-8);
// date
buf.setFontAlign(0,-1);
- var date = d.toString().substr(0,15);
+ var date = locale.date(d,false);
buf.drawString(date, buf.getWidth()/2, y+8);
flip();
}
diff --git a/apps/mclock/clock-morphing.json b/apps/mclock/clock-morphing.json
deleted file mode 100644
index bf629ac87..000000000
--- a/apps/mclock/clock-morphing.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name":"Morphing Clock","type":"clock",
- "icon":"*mclock",
- "src":"-mclock",
- "sortorder":-10
-}
diff --git a/apps/miclock/ChangeLog b/apps/miclock/ChangeLog
index 7819dbe2a..f2e354bc1 100644
--- a/apps/miclock/ChangeLog
+++ b/apps/miclock/ChangeLog
@@ -1 +1,3 @@
0.02: Modified for use with new bootloader and firmware
+0.03: Localization
+0.04: move jshint to the top
diff --git a/apps/miclock/clock-mixed.js b/apps/miclock/clock-mixed.js
index 579db5f19..bf6efb09e 100644
--- a/apps/miclock/clock-mixed.js
+++ b/apps/miclock/clock-mixed.js
@@ -1,4 +1,5 @@
/* jshint esversion: 6 */
+var locale = require("locale");
const Radius = { "center": 8, "hour": 78, "min": 95, "dots": 102 };
const Center = { "x": 120, "y": 132 };
@@ -16,6 +17,7 @@ function drawMixedClock() {
var date = new Date();
var dateArray = date.toString().split(" ");
+ var isEn = locale.name.startsWith("en");
var point = [];
var minute = date.getMinutes();
var hour = date.getHours();
@@ -25,10 +27,10 @@ function drawMixedClock() {
g.setColor(0x7be0);
g.setFont("6x8", 2);
g.setFontAlign(-1, 0);
- g.drawString(dateArray[0] + ' ', 4, 35, true);
- g.drawString(' ' + dateArray[2], 4, 225, true);
+ g.drawString(locale.dow(date,true) + ' ', 4, 35, true);
+ g.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 225, true);
g.setFontAlign(1, 0);
- g.drawString(dateArray[1], 237, 35, true);
+ g.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 35, true);
g.drawString(dateArray[3], 237, 225, true);
// draw hour and minute dots
diff --git a/apps/miclock/clock-mixed.json b/apps/miclock/clock-mixed.json
deleted file mode 100644
index b5396287a..000000000
--- a/apps/miclock/clock-mixed.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name":"Mixed Clock","type":"clock",
- "icon":"*miclock",
- "src":"-miclock",
- "sortorder":-10
-}
diff --git a/apps/minionclk/ChangeLog b/apps/minionclk/ChangeLog
new file mode 100755
index 000000000..7b83706bf
--- /dev/null
+++ b/apps/minionclk/ChangeLog
@@ -0,0 +1 @@
+0.01: First release
diff --git a/apps/minionclk/app-icon.js b/apps/minionclk/app-icon.js
new file mode 100755
index 000000000..f78fb9e35
--- /dev/null
+++ b/apps/minionclk/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwhAaXlZEolVVvOj0mq1XOv9/qtWFb8rquj1XV5wBDAA2jvMqNLMqwAsCABBhBAIujqpbWvIsCLowBHMg0qFyVVFQJNDK4YoEAYxjFvIuQwHP6ur5+rDgfU5wBDMI2qCYOsC4XV0bFOwIWBAAeBAIOrMYYsF54sCCIWswQDB52AGBcrFIOtF4gAEMoJfDAAOrCYQuDAIowKFwQWIBIeA52pGQPWLYgAFA4YDBGA4uDEwQYFGQvPL4IuKC4wwHFglWAIQYIYoQuFCYwDGqrqFF4YYCYBKeHHwgRLlZhCLowMBKIIubWAtWF4PXBQRdEBAIBHGAoTRCIRfCDQQvBLofXB4NVBIQcDFweAOYdWp9WqwrDGA8ABoRJFAAOswFOqtUwBNFeQYVCwMqp4ABqwbDCowvCAgKOGf4N5aAIwKKIVPpwRBGQOBFw5fCIgZfGwFVlT/BqtVBQQwFUAVWFwMAlYvCL6mBqkqDgNUF4RLGAgVPFwMAklPwD0GX5gOCXoReBJgSvDIwWrAoN6py/Cp58DCYxQBVIYwHqyQBbgL6EX4qQDFwRdFLAifBaoQaEAJIuDCYWrEgoTIGAerWIJfHGRZdECZ5fD0bQBIwJgEIoynEGBJxIAAYPBwHNlUq6owBMIZ4OMKQPC0eiqsr53PX4guOwBhOComk0ejqpfB53OJZAeDU4lVvN5OQQXKBIeA0RfBvNV5wwBMI4uD1oLDFoN4AQJEMBYWl0fOL4NUL4QwCPw4BEwN4AAejvGACZaMBLoRfGAIWr1Z8HwGkFQRIBDgekwAVGFoOAB4QTDqgvBFwQDD1Wk1el1YsBv4oDAIoAB0d/GQOlAIV/CpF51ZfFAIgAEUoOiAJYmEAAoHDvOBFxWqAIWpFxwnDOJABBquJ1QwM5oDCMYYDDAIN/5+r6CABHRKOBmVewIwCYY4sC0bGB678B1ekZYIAB0ulwOlvWkIQLJCMY0yq2sr2sMJYABp96vQnB0tPz4FBBANzAAWj5pdI0dWq0zr2Ir2rMJKQCvNIp9PudIuYDBAIV0FwSMFL4d/LoMzL4WBwIwD6hhH5ujuZXBFAIDDAIOdFwPNL4hdCwBdCq15AgMrAQLDB52pRYYACBIWjvGdK4NJAQOdvIlB5oXB0QwBLoWjqsrAAaSCGANVGAJHBSQjBEAAINBewIDCLYIBCAAJfDv5dCLAIvBvIEDAAJoBwGqDQSUBY4htDBwIBBAoIwDL4WjvNdhAvCAAYFGhDGCY4IAB1QABFwQwDv4BB1V/0eA0mALINdP4IpGMYcAAIQABGAIxDAAIFBruCroAGq1eFALkDmdeD4IjDGQYCCBQYDBCgIqBAJAoBAQIDCmeBCoIDCGgIfBLooADL4YBCJAIiCAANdAIQoCAI4ABAYdWKQQDDMooEBlVPBwJfCGAwABFxABDSAZYGLo1OvFOIgQaBLYZhFAIsyFgYAEFAUqpxUBFYQ="))
\ No newline at end of file
diff --git a/apps/minionclk/app.js b/apps/minionclk/app.js
new file mode 100755
index 000000000..88fe446ae
--- /dev/null
+++ b/apps/minionclk/app.js
@@ -0,0 +1,68 @@
+const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ulub7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBudJudPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNzAAIDGugGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMyHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1zzw0BDYI6B0R3DAAJ1BvMyp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw55CHwQABIQQBBABkzAILlCHQR1CFYavEPgsAAAIDEDQNdAwQAaHQNWEwQ0DHAh3KleBLoI7dHQKuFWQo0EAIsISoKdBHbyyHNgwADlVVpwEBDANWro7fd4Q6HO495vF5QgIYCd75eBHYUINAN5lQ3EA"));
+
+const locale = require("locale");
+
+const black = 0x0000;
+const white = 0xFFFF;
+
+let hour;
+let minute;
+let date;
+
+function draw() {
+ const d = new Date();
+
+ const newHour = ('0' + d.getHours()).substr(-2);
+ const newMinute = ('0' + d.getMinutes()).substr(-2);
+ const newDate = locale.date(d).trim();
+
+ g.setFontAlign(0, 0, 0);
+
+ if (newHour !== hour) {
+ g.setFontVector(48);
+ g.setColor(black);
+ g.drawString(hour, 64, 92);
+ g.setColor(white);
+ g.drawString(newHour, 64, 92);
+ hour = newHour;
+ }
+
+ if (newMinute !== minute) {
+ g.setFontVector(48);
+ g.setColor(black);
+ g.drawString(minute, 172, 92);
+ g.setColor(white);
+ g.drawString(newMinute, 172, 92);
+ minute = newMinute;
+ }
+
+ if (newDate !== date) {
+ g.setFontVector(12);
+ g.setColor(black);
+ g.drawString(date, 120, 228);
+ g.setColor(0xFFFF);
+ g.drawString(newDate, 120, 228);
+ date = newDate;
+ }
+}
+
+function drawAll() {
+ hour = '';
+ minute = '';
+ date = '';
+ g.drawImage(bob, 0, 0, { scale: 4 });
+ draw();
+}
+
+Bangle.on('lcdPower', function(on) {
+ if (on) {
+ drawAll();
+ }
+});
+
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+setInterval(draw, 1000);
+drawAll();
+
+setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
diff --git a/apps/minionclk/minionclk.png b/apps/minionclk/minionclk.png
new file mode 100755
index 000000000..77cac31df
Binary files /dev/null and b/apps/minionclk/minionclk.png differ
diff --git a/apps/mmonday/ChangeLog b/apps/mmonday/ChangeLog
new file mode 100644
index 000000000..1b5f725c4
--- /dev/null
+++ b/apps/mmonday/ChangeLog
@@ -0,0 +1 @@
+0.02: Added vibrate as beep workaround
diff --git a/apps/mmonday/manic-monday.js b/apps/mmonday/manic-monday.js
index a2245e7e7..c949fafbf 100644
--- a/apps/mmonday/manic-monday.js
+++ b/apps/mmonday/manic-monday.js
@@ -1,12 +1,23 @@
// made using https://www.espruino.com/Making+Music
// Manic Monday tone by The Bangles
-var SPEAKER_PIN = D18;
-
-function freq(f) {
- if (f===0) digitalWrite(SPEAKER_PIN, 0);
- else analogWrite(SPEAKER_PIN, 0.5, {freq: f});
+var s = require('Storage').readJSON('setting.json',1)||{};
+/* Normally we'd just use Bangle.beep which works automatically
+but because we're going lower level we need to account for the
+different pin. */
+if (s.beep=="vib") {
+ function freq(f) {
+ if (f===0) digitalWrite(D13, 0);
+ else analogWrite(D13, 0.1, {freq: f});
+ }
+} else {
+ function freq(f) {
+ if (f===0) digitalWrite(D18, 0);
+ else analogWrite(D18, 0.5, {freq: f});
+ }
}
+
+
freq(1000);
freq(1500);
freq(0);
@@ -39,6 +50,7 @@ var tune = "aggffefed";
var pos = 0;
setWatch(() => {
+ pos = 0;
var playing = setInterval(step, 500);
if(playing === 0) clearInterval(playing);
}, BTN1);
diff --git a/apps/mmonday/manic-monday.json b/apps/mmonday/manic-monday.json
deleted file mode 100644
index 1454658dd..000000000
--- a/apps/mmonday/manic-monday.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Manic Monday tone",
- "icon": "*mmonday",
- "src":"-mmonday"
-}
diff --git a/apps/moonphase/ChangeLog b/apps/moonphase/ChangeLog
new file mode 100644
index 000000000..baa668c3c
--- /dev/null
+++ b/apps/moonphase/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New App!
+0.02: Added GPS to obtain coordinates, added buttons
\ No newline at end of file
diff --git a/apps/moonphase/app-icon.js b/apps/moonphase/app-icon.js
new file mode 100644
index 000000000..b083b07e5
--- /dev/null
+++ b/apps/moonphase/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwgIifh/wAod//wECgP//+AAoMHAoPgCwQFBDAUfw///AFBjkD//8AoMYgPnEgUQgPB/4qCgeB/YFDwHxGAeA+AFEvHAAocdAoQCBh4CBgJFBh4CBAoNwg4FBhHA+AIBgkcgJSBAoMIg5SBAoIpB/E58EGAoP8n4FD/8f8EDAoQvBgfANYOfAYPwAoP/4AtBAoWAgP4SARfBAoZYB/0/Aod/AgKJCBQSVCj4FBUIStFXIrFFaIrdGADYA=="))
\ No newline at end of file
diff --git a/apps/moonphase/app.js b/apps/moonphase/app.js
new file mode 100644
index 000000000..480a0e144
--- /dev/null
+++ b/apps/moonphase/app.js
@@ -0,0 +1,353 @@
+//Icons from https://icons8.com
+//Sun and Moon calculations from https://github.com/mourner/suncalc and https://gist.github.com/endel/dfe6bb2fbe679781948c
+
+//varibales
+const storage = require('Storage');
+let coords;
+var timer;
+var fix;
+
+var PI = Math.PI,
+ sin = Math.sin,
+ cos = Math.cos,
+ tan = Math.tan,
+ asin = Math.asin,
+ atan = Math.atan2,
+ acos = Math.acos,
+ rad = PI / 180,
+ dayMs = 1000 * 60 * 60 * 24,
+ J1970 = 2440588,
+ J2000 = 2451545;
+
+var SunCalc = {};
+
+//pictures
+function getImg(i) {
+ var data = {
+ "NewMoon": "AD8AAH/4AHwPgDwA8BwADg4AAcMAADHAAA5gAAGYAABsAAAPAAADwAAA8AAAPAAADwAAA2AAAZgAAGcAADjAAAw4AAcHAAOA8APAHwPgAf/gAA/AAA==",
+ "WaxingCrescentNorth" : "AD8AAH/4AHw/gDwH8BwA/g4AH8MAB/HAAf5gAD+YAA/sAAP/AAD/wAA/8AAP/AAD/wAA/2AAP5gAD+cAB/jAAfw4AH8HAD+A8B/AHw/gAf/gAA/AAA==",
+ "WaningCrescentSouth" : "AD8AAH/4AHw/gDwH8BwA/g4AH8MAB/HAAf5gAD+YAA/sAAP/AAD/wAA/8AAP/AAD/wAA/2AAP5gAD+cAB/jAAfw4AH8HAD+A8B/AHw/gAf/gAA/AAA==",
+ "FirstQuarterNorth" : "AD8AAH/4AHx/gDwf8BwH/g4B/8MAf/HAH/5gB/+YAf/sAH//AB//wAf/8AH//AB//wAf/2AH/5gB/+cAf/jAH/w4B/8HAf+A8H/AHx/gAf/gAA/AAA==",
+ "FirstQuarterSouth" : "AD8AAH/4AH+PgD/g8B/4Dg/+AcP/gDH/4A5/+AGf/gBv/4AP/+AD//gA//4AP/+AD//gA3/4AZ/+AGf/gDj/4Aw/+AcH/gOA/4PAH+PgAf/gAA/AAA==",
+ "WaxingGibbousNorth" : "AD8AAH/4AH3/gDz/8Bw//g4f/8MH//HB//5g//+YP//sD///A///wP//8D///A///wP//2D//5g//+cH//jB//w4f/8HD/+A8//AH3/gAf/gAA/AAA==",
+ "WaxingGibbousSouth" : "AD8AAH/4AH/vgD/88B//Dg//4cP/+DH//g5//8Gf//Bv//wP//8D///A///wP//8D///A3//wZ//8Gf/+Dj//gw//4cH/8OA//PAH/vgAf/gAA/AAA==",
+ "FullMoon" : "AD8AAH/4AH//gD//8B///g///8P///H///5///+f///v/////////////////////////3///5///+f///j///w///8H//+A///AH//gAf/gAA/AAA==",
+ "WaningGibbousNorth" : "AD8AAH/4AH/vgD/88B//Dg//4cP/+DH//g5//8Gf//Bv//wP//8D///A///wP//8D///A3//wZ//8Gf/+Dj//gw//4cH/8OA//PAH/vgAf/gAA/AAA==",
+ "WaningGibbousSouth" : "AD8AAH/4AH3/gDz/8Bw//g4f/8MH//HB//5g//+YP//sD///A///wP//8D///A///wP//2D//5g//+cH//jB//w4f/8HD/+A8//AH3/gAf/gAA/AAA==",
+ "LastQuarterNorth" : "AD8AAH/4AH+PgD/g8B/4Dg/+AcP/gDH/4A5/+AGf/gBv/4AP/+AD//gA//4AP/+AD//gA3/4AZ/+AGf/gDj/4Aw/+AcH/gOA/4PAH+PgAf/gAA/AAA==",
+ "LastQuarterSouth" : "AD8AAH/4AHx/gDwf8BwH/g4B/8MAf/HAH/5gB/+YAf/sAH//AB//wAf/8AH//AB//wAf/2AH/5gB/+cAf/jAH/w4B/8HAf+A8H/AHx/gAf/gAA/AAA==",
+ "WaningCrescentNorth" : "AD8AAH/4AH8PgD+A8B/ADg/gAcP4ADH+AA5/AAGfwABv8AAP/AAD/wAA/8AAP/AAD/wAA38AAZ/AAGf4ADj+AAw/gAcH8AOA/gPAH8PgAf/gAA/AAA==",
+ "WaxingCrescentSouth" : "AD8AAH/4AH8PgD+A8B/ADg/gAcP4ADH+AA5/AAGfwABv8AAP/AAD/wAA/8AAP/AAD/wAA38AAZ/AAGf4ADj+AAw/gAcH8AOA/gPAH8PgAf/gAA/AAA=="
+ };
+ return {
+ width : 26, height : 26, bpp : 1,
+ transparent : 0,
+ buffer : E.toArrayBuffer(atob(data[i]))
+ };
+}
+// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
+// date/time constants and conversions
+function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
+function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); }
+function toDays(date) { return toJulian(date) - J2000; }
+
+// general calculations for position
+var e = rad * 23.4397; // obliquity of the Earth
+function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
+function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
+function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
+function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
+function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
+function astroRefraction(h) {
+ if (h < 0) // the following formula works for positive altitudes only.
+ h = 0; // if h = -0.08901179 a div/0 would occur.
+
+ // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+ // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
+ return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
+}
+
+// general sun calculations
+function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
+function eclipticLongitude(M) {
+
+ var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
+ P = rad * 102.9372; // perihelion of the Earth
+
+ return M + C + P + PI;
+}
+
+function sunCoords(d) {
+ var M = solarMeanAnomaly(d),
+ L = eclipticLongitude(M);
+ return {
+ dec: declination(L, 0),
+ ra: rightAscension(L, 0)
+ };
+}
+
+
+
+// adds a custom time to the times config
+SunCalc.addTime = function (angle, riseName, setName) {
+ times.push([angle, riseName, setName]);
+};
+
+// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
+function moonCoords(d) { // geocentric ecliptic coordinates of the moon
+ var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
+ M = rad * (134.963 + 13.064993 * d), // mean anomaly
+ F = rad * (93.272 + 13.229350 * d), // mean distance
+ l = L + rad * 6.289 * sin(M), // longitude
+ b = rad * 5.128 * sin(F), // latitude
+ dt = 385001 - 20905 * cos(M); // distance to the moon in km
+
+ return {
+ ra: rightAscension(l, b),
+ dec: declination(l, b),
+ dist: dt
+ };
+}
+
+SunCalc.getMoonPosition = function (date, lat, lng) {
+
+ var lw = rad * -lng,
+ phi = rad * lat,
+ d = toDays(date),
+ c = moonCoords(d),
+ H = siderealTime(d, lw) - c.ra,
+ h = altitude(H, phi, c.dec),
+ // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+ pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
+ h = h + astroRefraction(h); // altitude correction for refraction
+ return {
+ azimuth: azimuth(H, phi, c.dec),
+ altitude: h,
+ distance: c.dist,
+ parallacticAngle: pa
+ };
+};
+
+// calculations for illumination parameters of the moon,
+// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
+// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
+SunCalc.getMoonIllumination = function (date) {
+ var year = date.getFullYear();
+ var month = date.getMonth();
+ var day = date.getDate();
+ var Moon = {
+ phases: ['new', 'waxing-crescent', 'first-quarter', 'waxing-gibbous', 'full', 'waning-gibbous', 'last-quarter', 'waning-crescent'],
+ phase: function (year, month, day) {
+ let c = 0;
+ let e = 0;
+ let jd = 0;
+ let b = 0;
+ if (month < 3) {
+ year--;
+ month += 12;
+ }
+ ++month;
+ c = 365.25 * year;
+ e = 30.6 * month;
+ jd = c + e + day - 694039.09; // jd is total days elapsed
+ jd /= 29.5305882; // divide by the moon cycle
+ b = parseInt(jd); // int(jd) -> b, take integer part of jd
+ jd -= b; // subtract integer part to leave fractional part of original jd
+ b = Math.round(jd * 8); // scale fraction from 0-8 and round
+ if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0
+ return {phase: b, name: Moon.phases[b]};
+ }
+ };
+ return (Moon.phase(year, month, day));
+};
+
+function hoursLater(date, h) {
+ return new Date(date.valueOf() + h * dayMs / 24);
+}
+
+// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
+SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
+ var t = new Date(date);
+ if (inUTC) t.setUTCHours(0, 0, 0, 0);
+ else t.setHours(0, 0, 0, 0);
+ var hc = 0.133 * rad,
+ h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
+ h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
+
+ // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
+ for (var i = 1; i <= 24; i += 2) {
+ h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
+ h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
+ a = (h0 + h2) / 2 - h1;
+ b = (h2 - h0) / 2;
+ xe = -b / (2 * a);
+ ye = (a * xe + b) * xe + h1;
+ d = b * b - 4 * a * h1;
+ roots = 0;
+ if (d >= 0) {
+ dx = Math.sqrt(d) / (Math.abs(a) * 2);
+ x1 = xe - dx;
+ x2 = xe + dx;
+ if (Math.abs(x1) <= 1) roots++;
+ if (Math.abs(x2) <= 1) roots++;
+ if (x1 < -1) x1 = x2;
+ }
+ if (roots === 1) {
+ if (h0 < 0) rise = i + x1;
+ else set = i + x1;
+ } else if (roots === 2) {
+ rise = i + (ye < 0 ? x2 : x1);
+ set = i + (ye < 0 ? x1 : x2);
+ }
+ if (rise && set) break;
+ h0 = h2;
+ }
+ var result = {};
+ if (rise) result.rise = hoursLater(t, rise);
+ if (set) result.set = hoursLater(t, set);
+ if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
+ return result;
+};
+
+function getMPhaseComp (offset) {
+ var date = new Date();
+ date.setDate(date.getDate() + offset);
+ var dd = String(date.getDate());
+ if(dd<10){dd='0'+dd;}
+ var mm = String(date.getMonth() + 1);
+ if(mm<10){mm='0'+mm;}
+ var yyyy = date.getFullYear();
+ var phase = SunCalc.getMoonIllumination(date);
+ return dd + "." + mm + "." + yyyy + ": "+ phase.name;
+}
+
+function getMPhaseSim (offset) {
+ var date = new Date();
+ date.setDate(date.getDate() + offset);
+ var dd = String(date.getDate());
+ if(dd<10){dd='0'+dd;}
+ var mm = String(date.getMonth() + 1);
+ if(mm<10){mm='0'+mm;}
+ var yyyy = date.getFullYear();
+ var phase = SunCalc.getMoonIllumination(date);
+ return phase.name;
+}
+
+function drawMoonPhase(offset, x, y){
+ if (coords.lat >= 0 && coords.lat <= 90){ //Northern hemisphere
+ if (getMPhaseSim(offset) == "new") {g.drawImage(getImg("NewMoon"), x, y);}
+ if (getMPhaseSim(offset) == "waxing-crescent") {g.drawImage(getImg("WaxingCrescentNorth"), x, y);}
+ if (getMPhaseSim(offset) == "first-quarter") {g.drawImage(getImg("FirstQuarterNorth"), x, y);}
+ if (getMPhaseSim(offset) == "waxing-gibbous") {g.drawImage(getImg("WaxingGibbousNorth"), x, y);}
+ if (getMPhaseSim(offset) == "full") {g.drawImage(getImg("FullMoon"), x, y);}
+ if (getMPhaseSim(offset) == "waning-gibbous") {g.drawImage(getImg("WaningGibbousNorth"), x, y);}
+ if (getMPhaseSim(offset) == "last-quarter") {g.drawImage(getImg("LastQuarterNorth"), x, y);}
+ if (getMPhaseSim(offset) == "waning-crescent") {g.drawImage(getImg("WaningCrescentNorth"), x, y);}
+}
+ else { //Southern hemisphere
+ if (getMPhaseSim(offset) == "new") {g.drawImage(getImg("NewMoon"), x, y);}
+ if (getMPhaseSim(offset) == "waxing-crescent") {g.drawImage(getImg("WaxingCrescentSouth"), x, y);}
+ if (getMPhaseSim(offset) == "first-quarter") {g.drawImage(getImg("FirstQuarterSouth"), x, y);}
+ if (getMPhaseSim(offset) == "waxing-gibbous") {g.drawImage(getImg("WaxingGibbousSouth"), x, y);}
+ if (getMPhaseSim(offset) == "full") {g.drawImage(getImg("FullMoon"), x, y);}
+ if (getMPhaseSim(offset) == "waning-gibbous") {g.drawImage(getImg("WaningGibbousSouth"), x, y);}
+ if (getMPhaseSim(offset) == "last-quarter") {g.drawImage(getImg("LastQuarterSouth"), x, y);}
+ if (getMPhaseSim(offset) == "waning-crescent") {g.drawImage(getImg("WaningCrescentSouth"), x, y);}
+ }
+}
+
+function drawMoon(offset, x, y) {
+ g.setFont("6x8");
+ g.clear();
+ g.drawString("Key1: day+, Key2:today, Key3:day-",x,y-30);
+ g.drawString("Last known coordinates: " + coords.lat.toFixed(4) + " " + coords.lon.toFixed(4), x, y-20);
+ g.drawString("Press BTN4 to update",x, y-10);
+
+ g.drawString(getMPhaseComp(offset),x,y+30);
+ drawMoonPhase(offset, x+35, y+40);
+
+ g.drawString(getMPhaseComp(offset+2),x,y+70);
+ drawMoonPhase(offset+2, x+35, y+80);
+
+ g.drawString(getMPhaseComp(offset+4),x,y+110);
+ drawMoonPhase(offset+4, x+35, y+120);
+
+ g.drawString(getMPhaseComp(offset+6),x,y+150);
+ drawMoonPhase(offset+6, x+35, y+160);
+}
+
+//Write coordinates to file
+function updateCoords() {
+ storage.write('coords.json', coords);
+}
+
+//set coordinates to default (city where I live)
+function resetCoords() {
+ coords = {
+ lat : 52.96236,
+ lon : 7.62571,
+ };
+ updateCoords();
+}
+
+function getGpsFix() {
+ Bangle.on('GPS', function(fix) {
+ g.clear();
+
+ if (fix.fix == 1) {
+ var gpsString = "lat: " + fix.lat.toFixed(4) + " lon: " + fix.lon.toFixed(4);
+ coords.lat = fix.lat;
+ coords.lon = fix.lon;
+ updateCoords();
+ g.drawString("Got GPS fix and wrote coords to file",10,20);
+ g.drawString(gpsString,10,30);
+ g.drawString("Press BTN5 to return to app",10,40);
+ clearInterval(timer);
+ timer = undefined;
+ }
+ else {
+ g.drawString("Searching satellites...",10,20);
+ g.drawString("Press BTN5 to stop GPS",10, 30);
+ }
+ });
+}
+
+function start() {
+ var x = 10;
+ var y = 50;
+ var offsetMoon = 0;
+ coords = storage.readJSON('coords.json',1); //read coordinates from file
+ if (!coords) resetCoords(); //if coordinates could not be read, reset them
+ drawMoon(offsetMoon, x, y); //offset, x, y
+
+ //define button functions
+ setWatch(function() { //BTN1
+ offsetMoon++; //jump to next day
+ drawMoon(offsetMoon, x, y); //offset, x, y
+ }, BTN1, {edge:"rising", debounce:50, repeat:true});
+
+ setWatch(function() { //BTN2
+ offsetMoon = 0; //jump to today
+ drawMoon(offsetMoon, x, y); //offset, x, y
+ }, BTN2, {edge:"rising", debounce:50, repeat:true});
+
+ setWatch(function() { //BTN3
+ offsetMoon--; //jump to next day
+ drawMoon(offsetMoon, x, y); //offset, x, y
+ }, BTN3, {edge:"rising", debounce:50, repeat:true});
+
+ setWatch(function() { //BTN4
+ g.drawString("--- Getting GPS signal ---",x, y);
+ Bangle.setGPSPower(1);
+ timer = setInterval(getGpsFix, 10000);
+ }, BTN4, {edge:"rising", debounce:50, repeat:true});
+
+ setWatch(function() { //BTN5
+ if (timer) clearInterval(timer);
+ timer = undefined;
+ Bangle.setGPSPower(0);
+ drawMoon(offsetMoon, x, y); //offset, x, y
+ }, BTN5, {edge:"rising", debounce:50, repeat:true});
+}
+
+start();
\ No newline at end of file
diff --git a/apps/moonphase/app.png b/apps/moonphase/app.png
new file mode 100644
index 000000000..cdad713a1
Binary files /dev/null and b/apps/moonphase/app.png differ
diff --git a/apps/morse/morse-code.json b/apps/morse/morse-code.json
deleted file mode 100644
index bbd142c18..000000000
--- a/apps/morse/morse-code.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Morse Code","type":"app",
- "icon":"*morse",
- "src":"-morse"
-}
\ No newline at end of file
diff --git a/apps/nceuwid/nceu-widget.js b/apps/nceuwid/nceu-widget.js
deleted file mode 100644
index fa0e11cd1..000000000
--- a/apps/nceuwid/nceu-widget.js
+++ /dev/null
@@ -1,13 +0,0 @@
-(function(){
-var img = E.toArrayBuffer(atob("SxgCAAAAAAAAAAAAAAAAAAAAAAAAALwDwH/gD/0B//Af+AAD4C8f/wL8Dwf/8H//C//C//AAD9C8f/wL9Dw//+H//i4AD0AH8D/C8fAAL/Dz///H//y8ALgAf/D/i8fAALrzz///H//2//PAAv/Dz28f/gLz7z///H//29VPQAv/Dx+8fqQLw/y///H//y4ALwAP/Dwv8fAALwfw//9H//i+qD8FC0DwP8fAALwPwP/4H/+C//A//AADwD8fAAAAAAC/QD+gAAAAL4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGqooBkD+AP0fgvgAAAAAAAAAAL/88C4NBw0NBjQcAAAAAAAAAALQA8C4AAyQDBjAJAAAAAAAAAALQA8C4AAzACBjAKAAAAAAAAAAL/88C4ABTACRjQbAAAAAAAAAAL/48C4AHDACRgvjAAAAAAAAAALQA8C4AcDACBgACAAAAAAAAAALQA+D0BwCQDBgANAAAAAAAAAAL/8P/wHAA0NBgAoAAAAAAAAAAGqoC+AP/0HgAQuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"));
-var xpos = WIDGETPOS.tl;
-WIDGETPOS.tl+=75;
-
-
-WIDGETS["nceu"]={draw:()=>{
- var x = xpos, y = 0;
- g.setColor(0.17,0.2,0.5);
- g.drawImage(img,x,y);
- g.setColor(1,1,1);
-}};
-})()
diff --git a/apps/nceuwid/nceu-widget.json b/apps/nceuwid/nceu-widget.json
deleted file mode 100644
index b7c4faa70..000000000
--- a/apps/nceuwid/nceu-widget.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"nceuwid",
- "type":"widget",
- "src":"=nceuwid"
-}
diff --git a/apps/ncfrun/nceu-funrun.json b/apps/ncfrun/nceu-funrun.json
deleted file mode 100644
index 6a62f1a66..000000000
--- a/apps/ncfrun/nceu-funrun.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name":"5K Fun Run","type":"app",
- "icon":"*ncfrun",
- "src":"-ncfrun",
- "sortorder":-1
-}
diff --git a/apps/ncstart/ChangeLog b/apps/ncstart/ChangeLog
index ec926ceed..553f7388a 100644
--- a/apps/ncstart/ChangeLog
+++ b/apps/ncstart/ChangeLog
@@ -1,2 +1,4 @@
0.02: Modified for use with new bootloader and firmware
Renamed as nodeconf-specific
+0.03: Move configuration into App/widget settings
+ Move loader into welcome.boot.js
diff --git a/apps/ncstart/boot.js b/apps/ncstart/boot.js
new file mode 100644
index 000000000..dbb70d213
--- /dev/null
+++ b/apps/ncstart/boot.js
@@ -0,0 +1,10 @@
+(function() {
+ let s = require('Storage').readJSON('setting.json', 1) || {}
+ if (!s.welcomed && require('Storage').read('ncstart.app.js')) {
+ setTimeout(() => {
+ s.welcomed = true
+ require('Storage').write('setting.json', s)
+ load('ncstart.app.js')
+ })
+ }
+})()
diff --git a/apps/ncstart/settings.js b/apps/ncstart/settings.js
new file mode 100644
index 000000000..284262634
--- /dev/null
+++ b/apps/ncstart/settings.js
@@ -0,0 +1,16 @@
+// The welcome app is special, and gets to use global settings
+(function(back) {
+ let settings = require('Storage').readJSON('setting.json', 1) || {}
+ E.showMenu({
+ '': { 'title': 'NCEU Startup' },
+ 'Run again': {
+ value: !settings.welcomed,
+ format: v => v ? 'Yes' : 'No',
+ onchange: v => {
+ settings.welcomed = v ? undefined : true
+ require('Storage').write('setting.json', settings)
+ },
+ },
+ '< Back': back,
+ })
+})
diff --git a/apps/ncstart/start.js b/apps/ncstart/start.js
index 1a619555f..d2d713cb2 100644
--- a/apps/ncstart/start.js
+++ b/apps/ncstart/start.js
@@ -1,5 +1,3 @@
-NRF.sleep();
-var g = Graphics.getInstance();
g.setFontAlign(1, 1, 0);
const d = g.getWidth() - 18;
function c(a) {
@@ -53,7 +51,7 @@ function logos() {
];
function next() {
var n = logos.shift();
- var img = require("Storage").read("*"+n[0]);
+ var img = require("Storage").read("nc-"+n[0]+".img");
g.clear();
g.drawImage(img, n[1], n[2]);
n[3]();
@@ -116,22 +114,7 @@ function info() {
});
}
-function cleanup() {
- E.showMessage('Loading...');
- var s = require('Storage');
- s.erase('*nfr');
- s.erase('*nceu');
- s.erase('*bangle');
- s.erase('*nodew');
- s.erase('*tf');
- s.erase('+ncstart');
- s.erase('.boot3');
- s.erase('*ncstart');
- return Promise.resolve();
-}
-
welcome()
.then(logos)
.then(info)
- .then(cleanup)
.then(load);
diff --git a/apps/ncstart/start.json b/apps/ncstart/start.json
deleted file mode 100644
index 73665eb7f..000000000
--- a/apps/ncstart/start.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "NCEU Start",
- "type": "app",
- "icon": "*ncstart",
- "src": ".boot3"
-}
diff --git a/apps/nyancat/code.js b/apps/nyancat/code.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/openloc/openlocation-icon.js b/apps/openloc/app-icon.js
similarity index 100%
rename from apps/openloc/openlocation-icon.js
rename to apps/openloc/app-icon.js
diff --git a/apps/openloc/openlocation.js b/apps/openloc/app.js
similarity index 100%
rename from apps/openloc/openlocation.js
rename to apps/openloc/app.js
diff --git a/apps/openloc/openlocation.png b/apps/openloc/app.png
similarity index 100%
rename from apps/openloc/openlocation.png
rename to apps/openloc/app.png
diff --git a/apps/openloc/openlocation.json b/apps/openloc/openlocation.json
deleted file mode 100644
index c02a5a233..000000000
--- a/apps/openloc/openlocation.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Open Location","type":"app",
- "icon":"*openloc",
- "src":"-openloc"
-}
diff --git a/apps/pipboy/ChangeLog b/apps/pipboy/ChangeLog
new file mode 100644
index 000000000..134ba4e18
--- /dev/null
+++ b/apps/pipboy/ChangeLog
@@ -0,0 +1,2 @@
+0.01: New Watch!
+0.02: Changed colors for better readability and added current date
diff --git a/apps/pipboy/app-icon.js b/apps/pipboy/app-icon.js
new file mode 100644
index 000000000..935210156
--- /dev/null
+++ b/apps/pipboy/app-icon.js
@@ -0,0 +1 @@
+require("heatshrink").decompress(atob("mEwxH+AC4fDDjAuSgwACGFIuBhgACGAJjmLoQvDGAJkhbAguGGYwxbRAoAMGDZZMGBAvSbIpdSGCxYCW5jpDHhKSRDYQgGAwY8ETZYvPDZBXEAAwUFNAowOF44bFNJI2ENISRQdIqzLYhAxDAoRgScZgwFdow1DF54tQdpRMDF5jjHLiDxWF44wJBZIJFF5qPGF5blDLxIvPeAjsOF7RgCc6QvId6AvUd5QACF5pyEKAwlGF5qORcIwoFBgIwHFwwvTU4gvILxYuOXwouKA5AwGFxzuIDYYvkDQ6GIABKqEL6ryGF8QbHF6QXFX6wvuYIQvnFJgMBAAQva/wgMBQT4CF5QPCeB65CABgwCHxYuOF6L5JHYYuPSAyDFBRS6TMA4AME4RdHFyhgCLRLoJLzBgDExa7JFywwVFzQwTFwIADGDC5NRohDBSTQdCGCARCGDRNCGRQuFSobAYGAYxIFwoTDGKpMGWggADA4Q1FYipNGGA7NHYawVBD44qDGIbEHIwwlEA="))
diff --git a/apps/pipboy/app.js b/apps/pipboy/app.js
new file mode 100644
index 000000000..48a87fc5d
--- /dev/null
+++ b/apps/pipboy/app.js
@@ -0,0 +1,103 @@
+const bigFont = 4;
+const smallFont = 2;
+const tinyFont = 1;
+const green = 0x0661;
+const darkGreen = 0x0461;
+const darkerGreen = 0x0261;
+const pip = require("heatshrink").decompress(atob("klQyAlihNhgNhNP5FC0MjokboUJ8JC64ABBgNAjdDifiikhjfjjekjcDgOhImMKoUbscTsUbwUT4QBC8UMkMMgUT8MLwcBS8/hiNiIo2DhkBgkhhfhHoMT0MT0RLBjYJCCIMTocBUoIAiiIvBgcKsMSG4KLChY1CjckgUhgOAhJDBocL4RTBAYMT0ZJliS9BwUDQIchgWAhXCgVAgUghWhiXihWBgUAhdjhYTBkEB8ELkUTgcA8BHfgNAZ4MT8UTwcCJIOjikjihVB8QDBAIcToUSRYNDkdjjWDgOhjdjhVBI8EAgVBiXhQ4MTkcb4ZPCTILLBAYPBjZDBAIUL8QBBd4KRBWIMboZHfiPCOIMKsK5BR4XChkBAIUChkiheibYMT0RPBBoJZBgWgiRfD0RHfhMhhPhjcjhcBiQrBwSHBRocLRINCgUAhWBR4SZBsatCI4IRBsRHfAAMKkJBBhYBCgfBgZDBAYQFC0Q3BicCgehgbRBscKoMCkBlBjXiI8IxBifDgVAidDhfiIoMLkMUgUb4USQ4MiAoLlDiejgVhI4L3BjUjIr8BIILPBwUBwEa8YzBhQ/BoTLBjejgOgiTVEhkChdhiXCI4MB4MA8BHgsAxBjkDP4MjskTgZ3BTIMMkMb8cKLINCS4KPEcIMChWiK4LVhgJ5EAIJJBOoKTBbIQLB4I9BA4QJDCoOigZLBocJ8JHiwELGoPAgfghdCBIJ7BH4mhAYXAAYMDAIQNE0UKwJHioETwZ/C8cT0cS4UDgCBC4UTHIKjCiaNChfiB4SVDoUA4BJhicjhVgjeEjfDifiikCiZPBwUS4MB4ES0Ub4UUgMMgJDBAYJTBiXjIsIABiPiaIK5BgWggXhhXhgQJB4Mi8kakRJBHoJHDLIViiWCWYJHjhNBhWCHoMK0MbgkbkkTgYHBKYMTgUC4DVCcYUbscB8BDjAArFBOoKLCoECgADBaoMToUTsMLwML0MLBIUJ4JFpAAK3BidDhfiQIXCQIIBBRIcEgJFC0UKoJFrbYnBjeDibHBAIUMgIFC8QBC4UKsEA8EZ4cRJd0BwDPC8Y/BI4IBBR4IHBheijXkgOBjcCjcDVoJHriUiiWigVhiY3BS4PBI4JLCwcKkUK0UMgULwSrBiPBRtEhidDQII3BgMgPoMKgRRBhVhgPAiWBieCiehhQBBoKpBJYJFjiPihPhhdChfBieiGIMKwEKkI5BJIMTsUL4UL4afBgUgidjBIMbscJsBFfhOhjdDicigUAhWBYYMT8MT4QBHcIMCwIVBU4TnCMIMbgarBaLkgIoML8UT8Q1BiViYYI/DikCAoRXCgOAiWCCoIbBAIRHBEIPjBoJHbiR7D8UMgQ9BIoMKbIIJCAIMLkMSoSjCIYUMgIBECYIFCKYL/BIq8KgMTwRtBXIcL0UKsELRIIJBBYUCwEK4UL4UD4MDCoPgCIITEEIYJBgUBsLTUsMTka1DAIS3B0UCoETQYIvCT4MKoLXBH4QXCAYIBDEInhSIIZCsTTUsQZBNIXigkBGIVjgUhZIQ7DTIIPBoSHESoRDFAIYlBT4MToUBkCNQsETsRFBgaFBAoPhEIUiR4WiB4QxBwK+BhYTCZojPDAIYJCgfgAoRjBkKNQ0UTZoLVBgMKOYPihiLBoYhBToQJBXocALYbTEEIJHFE4IJCUYQFBkUA8CNMPoOCGIIBCoLPC4aPCsTNCBoIvD4MCkC/BHYrZEAoKTGdIQDB0UJ4KNMsR9CDYeCgR9BwZ9CsQHBGYhHB4RHGJIT1CNoTdGMIQnC0MSwSNKgEbobBBYYQXCgQrBkYpCJ4T9BI4sKkELoQzEMoKtB0BhBQYKnCT4Z5EjdCHoIAHTYIbBZYIBBDIWihWBhWhhYBBI4UDsJPCDIOhI4TjBD4PBAIJ9C8SdCkQdBBYIbCEoITDwUJ0JHHhTLBe4xPBhUigVBA4TNBoIhBA4KjC8RZBhcCDoKvDC4cTgUCkMbwgPDXoIfDikCiWBIw3ggOAidjI4ZFBiXCBoWgS4JHEoRZDBYMK4MKB4Q1BI4YDC4USsUKAoOjF4TrCAIcbkSNGoQZCeoODFYMboZRBB4LvBicjjeDCoMLoSBER4PhhWCV4StDJ4UbscSkQhBiXjjZJCLIsTsUBsBHDGoWChVhgUBhVBiPiLAnAhPgB4MJNoLJCQoJHBichTYWjAIJXBKIMCsMKkLHBjWDhOhOYJHFLocB8A0BhNhLYMLwJTBjUjiPChNBIwYFBjXjiXhiVha4RLB4ADBDYMCkEB4ABCLoQZBCoL9BjcjfoILBDIMD8AhCAISBBGoMR0USDIIbCEoMS0ZlBI4cBIINjhkCNIXiOIZzCZoODc4IBBSYQJBCYUT4cTgcA0DLBieCBYTXCAoMSwZHCwSJBV4JJBN4MST4Mga4ngY4MLoMbGYJrDW4QtC4RLBAIRDBE4INB0UKOYNggEhgFgiWiCYQPB8BPBhUCYoTRBokCwEbskJ4KzBhUia4j/B4cCsC3CgKlBAIXCKIQvCIIIvBjcidoJvCkMa8UBgMAdYPBjahCjbPB8MKGYQ3BjUDgPhjcEhUhiWhV4MJwTXCsDDCBYMCiVjhUBTIL7BKIIBBgVAhQ3B4BDCOoPjDoa5CB4MAhWCGYI3BEoMjkkSoQ3CoMSkZJBgPgNYITBhIPDoELwUDV4PBidjidDiXihXChWBF4IxCSYMCidihehhfAgfADIL7Ba4JBCwMCDYInBoTZCoDJDiNikXkiQpBoUZ8YNDM4MLsUD8EEgAvBGYI3CHIgNBAIYVBC4cL4T3BEYKZBjdjNYIBBCYYjBWII5DAAMJ0MawbjDAAnAjcje4MLSIPiGIJrBegMTOIPhB4IFEAIITBBYUCgMSAIKDBgKLC0ULsJFBWoMJ4I7GABgXBFYMMgIBBJILNBXYMT0JDBHoTpCLYQDD4USW4gnCI4NhhSzBkMSwcBR4wANhViI4aRDV4MKKYRDBBogBEJ4RdCf4sTgUTIYJpBoMRwRFTcocTsR7E0UCkELgQHCAJo5CI4lgI4MCoMasZPBADHgbIIxEwUCgELkQHC0BDHgYDB0IBBgPhNosa4cBkEA4BGZbIUiieiYYWjgPgTIwBKieCRIIjDZoMR4ZDbWYmAidCI41jA4RJBAI6nBBoNiV4I/fABMSwUb4RHDjejikCAJphBI9cBoETGINigUAhbfBapkL4UK4UJ4MKDAIAohOgjXjgUgheBhfAgY9B0IBBAoIHCAIOihVCiXCiXDI9JJCwMCgKPCI4YBIieCgPhicjjWEI9YABhUhY4I9C8JDEAoIBCdYJHCLwJGtI4NCikDikCAIsMAIUT8RDBCYMbwapBR+iRFRoeihVhhWhdYMS0RHtgUhgY1B0EDAIPAAoQDB4MToUCgELgMDA4MiR+sDR4Q9BBIUhgPgieCgYDBoRHwaYoBD8DPDa4IJBhkAiXjI91BifihkCifhAoMUgUMgMb4cKgMSsQNBhkhJ4JHugKNCIoIBCA4cbocBsEJ4MT0RVBgUBI9sJHoOhaIQDB4EDBIUSsITDjWjkcjgMgI9sB4BHEAIPgSoVihOhLYkBiNCItpHC0CLDAYMDI4OijXjHt5HKwET4cL8UTAIPjiciTYMI8BH4gES4ULkcS8USkUJ8AJBiTPwAA8KsUKsMCoECsMK0MTgUTsZH1hPhhdChaNB4MT0RBC4cKTINCgMgImGghPihdigfBgeggfAhehhXBgHAZ+qLCwULAYPCiaNBoTbBgNAIuoABiOiiQBB0LJBhUhgKLBAEgA=="));
+
+function topLine() {
+
+ g.setColor(green);
+ g.setFontAlign(0, -1, 0);
+
+ g.moveTo(0, 50).lineTo(30, 50);
+ g.lineTo(30, 40);
+ g.lineTo(35, 40).moveTo(90, 40).lineTo(95, 40);
+ g.lineTo(95, 50);
+ g.lineTo(239, 50);
+
+ g.setFont("6x8", smallFont);
+ g.drawString("STAT", 65, 34);
+ g.drawString("INV", 130, 34);
+ g.drawString("DATA", 190, 34);
+ g.setColor(darkGreen);
+ g.drawString("STATUS", 45, 55);
+ g.drawString("SPEC", 122, 55);
+ g.drawString("PERKS", 195, 55);
+}
+
+function bottomLine() {
+
+ //first line
+ g.setColor(darkerGreen);
+ g.fillRect(5, 175, 100, 185); //DATE
+ g.fillRect(105, 175, 160, 185);//STIM
+ g.fillRect(166, 175, 239, 185); // RADAWAY
+
+ g.setColor(green);
+ g.setFont("6x8", tinyFont);
+ g.drawString("DATE", 20, 177);
+ g.drawString("STIM (3)", 135, 177);
+ g.drawString("RADAWAY (8)", 205, 177);
+
+ //second line
+ g.setColor(darkerGreen);
+ g.fillRect(5, 190, 70, 200);
+ g.fillRect(75, 190, 239, 200);
+
+ g.setColor(green);
+ g.drawString("HP 115/115", 38, 192);
+ g.drawString("LEVEL 6", 100, 192);
+ g.drawRect(127, 192, 235, 198);
+}
+
+function boy() {
+ g.drawImage(pip, 165, 85);
+}
+
+function drawClock() {
+
+ var t = new Date();
+ var h = t.getHours();
+ var m = t.getMinutes();
+ var dd = t.getDate();
+ var mm = t.getMonth()+1; //month is zero-based
+ var yy = t.getFullYear();
+ var time = ("0" + h).substr(-2) + ":" + ("0" + m).substr(-2);
+
+ //create date string
+ if (dd.toString().length < 2) dd = '0' + dd;
+ if (mm.toString().length < 2) mm = '0' + mm;
+ var date = dd + "." + mm + "." + yy;
+
+ g.setFont("6x8",bigFont);
+ g.setColor(green);
+ g.setFontAlign(0, -1, 0);
+
+ g.clearRect(0, 110, 150, 140);
+ g.drawString(time, 70, 110);
+
+ //draw date
+ g.setFont("6x8", tinyFont);
+ g.drawString(date, 67, 177);
+}
+
+function drawAll() {
+ topLine();
+ boy();
+ bottomLine();
+ drawClock();
+}
+
+Bangle.on('lcdPower', function(on) {
+ if (on) drawAll();
+});
+
+g.clear();
+Bangle.loadWidgets();
+Bangle.drawWidgets();
+setInterval(drawClock, 1E4);
+drawAll();
+
+setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
diff --git a/apps/pipboy/app.png b/apps/pipboy/app.png
new file mode 100644
index 000000000..018b5c7bb
Binary files /dev/null and b/apps/pipboy/app.png differ
diff --git a/apps/pomodo/pomodoro.json b/apps/pomodo/pomodoro.json
deleted file mode 100644
index c017048df..000000000
--- a/apps/pomodo/pomodoro.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Pomodoro","type":"app",
- "icon":"*pomodo",
- "src":"-pomodo"
-}
diff --git a/apps/pparrot/party-parrot.json b/apps/pparrot/party-parrot.json
deleted file mode 100644
index abf950e47..000000000
--- a/apps/pparrot/party-parrot.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "name":"Party Parrot","type":"app",
- "icon":"*pparrot",
- "src":"-pparrot"
-}
\ No newline at end of file
diff --git a/apps/qrcode/qrcode.html b/apps/qrcode/qrcode.html
index cb734c1b5..5d372aa59 100644
--- a/apps/qrcode/qrcode.html
+++ b/apps/qrcode/qrcode.html
@@ -4,11 +4,11 @@
-