1
0
Fork 0

Merge remote-tracking branch 'upstream/master'

master
Danny 2022-02-01 23:53:33 +01:00
commit 235b61e5c0
621 changed files with 16390 additions and 7296 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ appdates.csv
_config.yml
tests/Layout/bin/tmp.*
tests/Layout/testresult.bmp
apps.local.json

View File

@ -12,7 +12,7 @@ and that it is not licensed in another way that would make this impossible.
## How does it work?
* A list of apps is in `apps.json`
* A list of apps is in `apps.json` (this is auto-generated from all the `apps/yourapp/metadata.json` using Jekyll or `bin/create_apps_json.sh`)
* Each element references an app in `apps/<id>` which is uploaded
* When it starts, BangleAppLoader checks the JSON and compares
it with the files it sees in the watch's storage.
@ -53,10 +53,10 @@ easily distinguish between file types, we use the following:
is limited to 28 char filenames and appends a file extension (eg `.js`) so please
try and keep filenames short to avoid overflowing the buffer.
* Create a folder called `apps/<id>`, lets assume `apps/myappid`
* We'd recommend that you copy files from 'Example Applications' (below) as a base, or...
* We'd recommend that you copy files from one of the Examples in `apps/_example_*` (see below), or...
* `apps/myappid/app.png` should be a 48px icon
* Use http://www.espruino.com/Image+Converter to create `apps/myappid/app-icon.js`, using a 1 bit, 4 bit or 8 bit Web Palette "Image String"
* Create an entry in `apps.json` as follows:
* Create/modify `apps/myappid/metadata.json` as follows:
```
{ "id": "myappid",
@ -116,8 +116,7 @@ and set it to `Load default application`.
To make the process easier we've come up with some example applications that you can use as a base
when creating your own. Just come up with a unique name (ideally lowercase, under 20 chars), copy `apps/_example_app`
or `apps/_example_widget` to `apps/myappid`, and add `apps/_example_X/add_to_apps.json` to
`apps.json`.
or `apps/_example_widget` to `apps/myappid`, and edit `apps/myappid/metadata.json` accordingly.
**Note:** the max filename length is 28 chars, so we suggest an app ID of under
20 so that when `.app.js`/etc gets added to the end the filename isn't cropped.
@ -131,7 +130,7 @@ The app example is available in [`apps/_example_app`](apps/_example_app)
Apps are listed in the Bangle.js menu, accessible from a clock app via the middle button.
* `add_to_apps.json` - insert into `apps.json`, describes the app to bootloader and loader
* `metadata.json` - describes the app to bootloader and loader
* `app.png` - app icon - 48x48px
* `app-icon.js` - JS version of the icon (made with http://www.espruino.com/Image+Converter) for use in Bangle.js's menu
* `app.js` - app code
@ -144,11 +143,11 @@ Use the Espruino [image converter](https://www.espruino.com/Image+Converter) and
Follow this steps to create a readable icon as image string.
1. upload a png file
1. upload a 48x48 png file - THE IMAGE SHOULD BE 48x48 OR LESS
2. set _X_ Use Compression
3. set _X_ Transparency (optional)
4. set Diffusion: _flat_
5. set Colours: _1 bit_, _4 bit_ or _8 bit Web Palette_
5. set Colours: _1 bit_, any of the Optimised options, or _8 bit Web Palette_ are best
6. set Output as: _Image String_
Replace this line with the image converter output:
@ -157,6 +156,8 @@ Replace this line with the image converter output:
require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA=="))
```
**Do not add a trailing semicolon**
You can also use this converter for creating images you like to draw with `g.drawImage()` with your app.
Apps that need widgets can call `Bangle.loadWidgets()` **once** at startup to load
@ -167,17 +168,18 @@ has call to completely clear the screen. Widgets themselves will update as and w
The widget example is available in [`apps/_example_widget`](apps/_example_widget)
* `add_to_apps.json` - insert into `apps.json`, describes the widget to bootloader and loader
* `metadata.json` - describes the widget to bootloader and loader
* `widget.js` - widget code
Widgets are just small bits of code that run whenever an app that supports them
calls `Bangle.loadWidgets()`. If they want to display something in the 24px high
widget bars at the top and bottom of the screen they can add themselves to
the global `WIDGETS` array with:
widget bar at the top of the screen they can add themselves to the global
`WIDGETS` array with:
```
WIDGETS["mywidget"]={
area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
area:"tl", // tl (top left), tr (top right)
sortorder:0, // (Optional) determines order of widgets in the same corner
width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
draw:draw // called to draw the widget
};
@ -202,7 +204,7 @@ and which gives information about the app for the Launcher.
// if it's 'clock' then it'll be loaded by default at boot time
// if this is 'bootloader' then it's code that is run at boot time, but is not in a menu
"version":"1.23",
// added by BangleApps loader on upload based on apps.json
// added by BangleApps loader on upload based on metadata.json
"files:"file1,file2,file3",
// added by BangleApps loader on upload - lists all files
// that belong to the app so it can be deleted
@ -214,7 +216,7 @@ and which gives information about the app for the Launcher.
}
```
### `apps.json` format
### `metadata.json` format
```
{ "id": "appid", // 7 character app id
@ -293,9 +295,9 @@ and which gives information about the app for the Launcher.
* storage is used to identify the app files and how to handle them
* data is used to clean up files when the app is uninstalled
### `apps.json`: `custom` element
### `metadata.json`: `custom` element
Apps that can be customised need to define a `custom` element in `apps.json`,
Apps that can be customised need to define a `custom` element in `metadata.json`,
which names an HTML file in that app's folder.
When `custom` is defined, the 'upload' button is replaced by a customize
@ -303,7 +305,7 @@ button, and when clicked it opens the HTML page specified in an iframe.
In that HTML file you're then responsible for handling a button
press and calling `sendCustomizedApp` with your own customised
version of what's in `apps.json`:
version of what's in `metadata.json`:
```
<html>
@ -335,9 +337,9 @@ for a clean example.
and will never be loaded. This is so the app loader can tell if it's a JavaScript
file based on the extension, and if so it can minify and pretokenise it.
### `apps.json`: `interface` element
### `metadata.json`: `interface` element
Apps that create data that can be read back can define a `interface` element in `apps.json`,
Apps that create data that can be read back can define a `interface` element in `metadata.json`,
which names an HTML file in that app's folder.
When `interface` is defined, a `Download from App` button is added to
@ -401,7 +403,7 @@ Example `settings.js`
E.showMenu(appMenu)
})
```
In this example the app needs to add `myappid.settings.js` to `storage` in `apps.json`.
In this example the app needs to add `myappid.settings.js` to `storage` in `metadata.json`.
It should also add `myappid.json` to `data`, to make sure it is cleaned up when the app is uninstalled.
```json
{ "id": "myappid",
@ -461,16 +463,13 @@ The screen is parted in a widget and app area for lcd mode `direct`(default).
| areas | as rectangle or point |
| :-:| :-: |
| Widget | (0,0,239,23) |
| Widget bottom bar (optional) | (0,216,239,239) |
| Apps | (0,24,239,239) (see below) |
| Apps | (0,24,239,239) |
| BTN1 | (230, 55) |
| BTN2 | (230, 140) |
| BTN3 | (230, 210) |
| BTN4 | (0,0,119, 239)|
| BTN5 | (120,0,239,239) |
- If there are widgets at the bottom of the screen, apps should actually keep the bottom 24px free, so should keep to the area (0,24,239,215)
- Use `g.setFontAlign(0, 0, 3)` to draw rotated string to BTN1-BTN3 with `g.drawString()`.
- For BTN4-5 the touch area is named
@ -515,7 +514,6 @@ The [`testing`](testing) folder contains snippets of code that might be useful f
* `testing/colors.js` - 16 bit colors as name value pairs
* `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

5679
apps.json

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"id": "1button",
"name": "One-Button-Tracker",
"version": "0.01",
"description": "A widget that turns BTN1 into a tracker, records time of button press/release.",
"icon": "widget.png",
"type": "widget",
"tags": "tool,quantifiedself,widget",
"supports": ["BANGLEJS"],
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"1button.wid.js","url":"widget.js"}
],
"data": [{"name":"one_button_presses.csv","storageFile":true}]
}

17
apps/93dub/metadata.json Normal file
View File

@ -0,0 +1,17 @@
{ "id": "93dub",
"name": "93 Dub",
"shortName":"93 Dub",
"icon": "93dub.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.06",
"description": "Fan recreation of orviwan's 91 Dub app for the Pebble smartwatch. Uses assets from his 91-Dub-v2.0 repo",
"tags": "clock",
"type": "clock",
"supports":["BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"93dub.app.js","url":"app.js"},
{"name":"93dub.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "BLEcontroller",
"name": "BLE Customisable Controller with Joystick",
"shortName": "BLE Controller",
"version": "0.01",
"description": "A configurable controller for BLE devices and robots, with a basic four direction joystick. Designed to be easy to customise so you can add your own menus.",
"icon": "BLEcontroller.png",
"tags": "tool,bluetooth",
"supports": ["BANGLEJS"],
"readme": "README.md",
"allow_emulator": false,
"storage": [
{"name":"BLEcontroller.app.js","url":"app.js"},
{"name":"BLEcontroller.img","url":"app-icon.js","evaluate":true}
]
}

15
apps/HRV/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{
"id": "HRV",
"name": "Heart Rate Variability monitor",
"shortName": "HRV monitor",
"version": "0.04",
"description": "Heart Rate Variability monitor, see Readme for more info",
"icon": "hrv.png",
"tags": "",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"HRV.app.js","url":"app.js"},
{"name":"HRV.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "UI4swatch",
"name": "UI 4 swatch",
"shortName": "UI 4 swatch",
"version": "0.01",
"description": "A UI/UX for espruino smartwatches, displays dinamically calc. x,y coordinates.",
"icon": "app.png",
"tags": "Color,input,buttons,touch,UI",
"supports": ["BANGLEJS"],
"readme": "README.md",
"screenshots": [{"url":"UI4swatch_icon.png"},{"url":"UI4swatch_s1.png"}],
"storage": [
{"name":"UI4swatch.app.js","url":"app.js"},
{"name":"UI4swatch.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -1,4 +1,3 @@
// Create an entry in apps.json as follows:
{ "id": "7chname",
"name": "My app's human readable name",
"shortName":"Short Name",

View File

@ -1,4 +1,3 @@
// Create an entry in apps.json as follows:
{ "id": "7chname",
"name": "My widget's human readable name",
"shortName":"Short Name",

View File

@ -0,0 +1,17 @@
{
"id": "a_clock_timer",
"name": "A Clock with Timer",
"version": "0.01",
"description": "A Clock with Timer, Map and Time Zones",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"a_clock_timer.app.js","url":"app.js"},
{"name":"a_clock_timer.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id":"a_speech_timer",
"name":"Speech Timer",
"icon": "app.png",
"version":"1.01",
"description": "A timer designed to help keeping your speeches and presentations to time.",
"tags": "tool,timer",
"readme":"README.md",
"supports":["BANGLEJS2"],
"screenshots": [{"url":"screenshot1.png"},{"url":"screenshot2.png"},{"url":"screenshot3.png"}],
"allow_emulator": true,
"storage": [
{"name":"a_speech_timer.app.js","url":"app.js"},
{"name":"a_speech_timer.img","url":"app-icon.js","evaluate":true}
]
}

17
apps/about/metadata.json Normal file
View File

@ -0,0 +1,17 @@
{
"id": "about",
"name": "About",
"version": "0.12",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"icon": "app.png",
"tags": "tool,system",
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"bangle1-about-screenshot.png"}],
"allow_emulator": true,
"storage": [
{"name":"about.app.js","url":"app-bangle1.js","supports": ["BANGLEJS"]},
{"name":"about.app.js","url":"app-bangle2.js","supports": ["BANGLEJS2"]},
{"name":"about.img","url":"app-icon.js","evaluate":true}
],
"sortorder": -4
}

View File

@ -101,8 +101,8 @@
<script>
$(function () {
let ClockSize, ClockSizeURL
let ClockFace, ClockFaceNumerals, ClockFaceDots, ClockFaceURL
let ClockHands, SecondHand, ClockHandsURL, FillColor
let ClockFace, ClockFaceURL, ClockFaceNumerals, ClockFaceDots
let ClockHands, ClockHandsURL, SecondHand, FillColor
let ComplicationTL, ComplicationTLURL
let ComplicationT, ComplicationTURL
let ComplicationTR, ComplicationTRURL
@ -118,8 +118,8 @@
function backupConfiguration () {
let Configuration = {
ClockSize, ClockSizeURL,
ClockFace, ClockFaceNumerals, ClockFaceDots, ClockFaceURL,
ClockHands, SecondHand, ClockHandsURL, FillColor,
ClockFace, ClockFaceURL, ClockFaceNumerals, ClockFaceDots,
ClockHands, ClockHandsURL, SecondHand, FillColor,
ComplicationTL, ComplicationTLURL,
ComplicationT, ComplicationTURL,
ComplicationTR, ComplicationTRURL,
@ -312,8 +312,8 @@
function chosenClockFace () {
switch (ClockFace) {
case 'none': return "undefined"
case 'four-fold': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-four-fold-clock-face/main/ClockFace.js')"
case 'twelve-fold': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-twelve-fold-clock-face/main/ClockFace.js')"
case 'four-numbered': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-four-numbered-clock-face/main/ClockFace.js')"
case 'twelve-numbered': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-twelve-numbered-clock-face/main/ClockFace.js')"
case 'rainbow': return "require('https://raw.githubusercontent.com/rozek/banglejs-2-rainbow-clock-face/main/ClockFace.js')"
case 'custom': return "require('" + ClockFaceURL + "')"
}
@ -412,7 +412,7 @@ console.log(AppSource)
}
$('input[type="radio"]').on('change',retrieveAndValidateInputs)
$('input[type="url"]'). on('change',retrieveAndValidateInputs)
$('input[type="url"]'). on('input', retrieveAndValidateInputs)
$('select'). on('change',retrieveAndValidateInputs)
$('#UploadButton').on('click',createAndUploadApp)
})
@ -485,23 +485,23 @@ console.log(AppSource)
<input type="radio" name="clock-face" value="none" checked>
<img src="none.png"/>
</label><br>
none
(none)
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="four-fold">
<img src="fourfoldClockFace.png"/>
<input type="radio" name="clock-face" value="four-numbered">
<img src="fournumberedClockFace.png"/>
</label><br>
four-fold
four-numbered
</td>
<td>
<label class="Preview">
<input type="radio" name="clock-face" value="twelve-fold">
<img src="twelvefoldClockFace.png"/>
<input type="radio" name="clock-face" value="twelve-numbered">
<img src="twelvenumberedClockFace.png"/>
</label><br>
twelve-fold
twelve-numbered
</td>
<td>
@ -521,25 +521,25 @@ console.log(AppSource)
</td>
</tr>
</tbody></table>
</p><p>
Clock faces are drawn in the configured foreground and background colors
(you may select them at the end of this form)
</p><p>
"Four-fold" clock faces may draw indian-arabic or roman numerals. Which do you prefer?
</p><p>
<input type="radio" name="clock-face-numerals" value="indian" checked> indian-arabic (3, 6, 9, 12)<br>
<input type="radio" name="clock-face-numerals" value="roman"> roman (III, VI, IX, XII)
</p><p>
The "twelve-fold" and "rainbow"-colored faces may be drawn with or without
dots marking the position of every minute. Which variant do you prefer?
</p><p>
<input type="radio" name="clock-face-dots" value="without-dots" checked> without dots <br>
<input type="radio" name="clock-face-dots" value="with-dots"> with dots
</p><p>
If you prefer a "custom" clock face, please enter the URL
of its JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-face-custom-url" size="50">
</p><p>
Clock faces are drawn in the configured foreground and background colors
(you may select them at the end of this form)
</p><p>
"Four-numbered" clock faces may draw indian-arabic or roman numerals. Which do you prefer?
</p><p>
<input type="radio" name="clock-face-numerals" value="indian" checked> indian-arabic (3, 6, 9, 12)<br>
<input type="radio" name="clock-face-numerals" value="roman"> roman (III, VI, IX, XII)
</p><p>
The "twelve-numbered" and "rainbow"-colored faces may be drawn with or without
dots marking the position of every minute. Which variant do you prefer?
</p><p>
<input type="radio" name="clock-face-dots" value="without-dots" checked> without dots <br>
<input type="radio" name="clock-face-dots" value="with-dots"> with dots
</p>
<h3>Clock Hands</h3>
@ -582,6 +582,11 @@ console.log(AppSource)
</td>
</tr>
</tbody></table>
</p><p>
If you prefer "custom" clock hands, please enter the URL
of their JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-hands-custom-url" size="50">
</p><p>
Clock hands are drawn in the configured foreground and background colors
(you may select them at the end of this form)
@ -631,11 +636,6 @@ console.log(AppSource)
<input type="radio" name="second-hand" value="#FF00FF" class="ColorPatch" style="background:#FF00FF"/>
<input type="radio" name="second-hand" value="#00FFFF" class="ColorPatch" style="background:#00FFFF"/>
<input type="radio" name="second-hand" value="#FFFFFF" class="ColorPatch" style="background:#FFFFFF"/>
</p><p>
If you prefer "custom" clock hands, please enter the URL
of their JavaScript module below:
</p><p>
custom URL: <input type="url" id="clock-hands-custom-url" size="50">
</p>
<h3>Complications</h3>

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

18
apps/ac_ac/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{ "id": "ac_ac",
"name": "A Configurable Analog Clock",
"shortName":"Configurable Clock",
"version":"0.03",
"description": "AC-AC, a highly customizable analog clock with several clock faces, hands and complications to choose from",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": false,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"custom": "Customizer.html",
"storage": [
{"name":"ac_ac.app.js","url":"app.js"},
{"name":"ac_ac.img","url":"app-icon.js","evaluate":true}
]
}

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,14 @@
{ "id": "accelgraph",
"name": "Accelerometer Graph",
"shortName":"Accel Graph",
"version":"0.01",
"description": "A simple app to draw a graph of data from the accelerometer on the screen",
"icon": "app.png",
"tags": "tool,debug",
"supports" : ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}],
"storage": [
{"name":"accelgraph.app.js","url":"app.js"},
{"name":"accelgraph.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "accellog",
"name": "Acceleration Logger",
"shortName": "Accel Log",
"version": "0.03",
"description": "Logs XYZ acceleration data to a CSV file that can be downloaded to your PC",
"icon": "app.png",
"tags": "outdoor",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"accellog.app.js","url":"app.js"},
{"name":"accellog.img","url":"app-icon.js","evaluate":true}
],
"data": [{"wildcard":"accellog.?.csv"}]
}

View File

@ -0,0 +1,17 @@
{
"id": "accelrec",
"name": "Acceleration Recorder",
"shortName": "Accel Rec",
"version": "0.02",
"description": "This app puts the Bangle's accelerometer into 100Hz mode and reads 2 seconds worth of data after movement starts. The data can then be exported back to the PC.",
"icon": "app.png",
"tags": "",
"supports": ["BANGLEJS"],
"readme": "README.md",
"interface": "interface.html",
"storage": [
{"name":"accelrec.app.js","url":"app.js"},
{"name":"accelrec.img","url":"app-icon.js","evaluate":true}
],
"data": [{"wildcard":"accelrec.?.csv"}]
}

16
apps/aclock/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"id": "aclock",
"name": "Analog Clock",
"version": "0.15",
"description": "An Analog Clock",
"icon": "clock-analog.png",
"screenshots": [{"url":"screenshot_analog.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"aclock.app.js","url":"clock-analog.js"},
{"name":"aclock.img","url":"clock-analog-icon.js","evaluate":true}
]
}

View File

@ -1 +1,3 @@
0.01: New App!
0.02: Faster maze generation
0.03: Avoid clearing bottom widgets

View File

@ -11,13 +11,10 @@ function Maze(n) {
this.margin = Math.floor((g.getHeight()-this.total_length)/2);
this.ball_x = 0;
this.ball_y = 0;
this.clearScreen = function() {
g.clearRect(
0, this.margin,
g.getWidth(), this.margin+this.total_length
);
};
this.clearScreen();
// This voodoo is needed because otherwise
// bottom line widgets (like digital clock)
// disappear during maze generation
Bangle.drawWidgets();
g.setColor(g.theme.fg);
for (let i=0; i<=n; i++) {
g.drawRect(
@ -35,21 +32,56 @@ function Maze(n) {
this.walls[cell] = WALL_RIGHT|WALL_DOWN;
this.groups[cell] = cell;
}
// Candidates of walls to break when digging the maze.
// If candidate failed (breaking it would create a loop),
// it would never succeed, so no need to retry it.
let candidates_down = [],
candidates_right = [];
for (let r=0 ; r<n; r++) {
for (let c=0; c<n; c++) {
let cell = n*r+c;
if (r<(n-1)) { // Don't break wall down for bottom row.
candidates_down.push(cell);
}
if (c<(n-1)) { // Don't break wall right for rightmost column.
candidates_right.push(cell);
}
}
}
let from_group, to_group;
let ngroups = n*n;
while (--ngroups) {
// Abort if BTN1 pressed [grace period for menu]
// (for some reason setWatch() fails inside constructor)
if (ngroups<n*n-4 && digitalRead(BTN1)) {
if (ngroups<n*n-16 && digitalRead(BTN1)) {
aborting = true;
return;
}
from_group = to_group = -1;
while (from_group<0) {
if (Math.random()<0.5) { // try to break a wall right
let r = Math.floor(Math.random()*n);
let c = Math.floor(Math.random()*(n-1));
let cell = r*n+c;
let trying_down = false;
if (Math.random()<0.5 && candidates_down.length || !candidates_right.length) {
trying_down = true;
}
let candidates = trying_down ? candidates_down : candidates_right,
candidate_index = Math.floor(Math.random()*candidates.length),
cell = candidates.splice(candidate_index, 1)[0],
r = Math.floor(cell/n),
c = cell%n;
if (trying_down) { // try to break a wall down
if (this.groups[cell]!=this.groups[cell+n]) {
this.walls[cell] &= ~WALL_DOWN;
g.clearRect(
this.margin+c*this.wall_length+1,
this.margin+(r+1)*this.wall_length,
this.margin+(c+1)*this.wall_length-1,
this.margin+(r+1)*this.wall_length
);
g.flip(); // show progress.
from_group = this.groups[cell];
to_group = this.groups[cell+n];
}
} else { // try to break a wall right
if (this.groups[cell]!=this.groups[cell+1]) {
this.walls[cell] &= ~WALL_RIGHT;
g.clearRect(
@ -62,21 +94,6 @@ function Maze(n) {
from_group = this.groups[cell];
to_group = this.groups[cell+1];
}
} else { // try to break a wall down
let r = Math.floor(Math.random()*(n-1));
let c = Math.floor(Math.random()*n);
let cell = r*n+c;
if (this.groups[cell]!=this.groups[cell+n]) {
this.walls[cell] &= ~WALL_DOWN;
g.clearRect(
this.margin+c*this.wall_length+1,
this.margin+(r+1)*this.wall_length,
this.margin+(c+1)*this.wall_length-1,
this.margin+(r+1)*this.wall_length
);
from_group = this.groups[cell];
to_group = this.groups[cell+n];
}
}
}
for (let cell = 0; cell<n*n; cell++) {
@ -85,11 +102,6 @@ function Maze(n) {
}
}
}
this.clearScreen = function() {
g.clearRect(
0, MARGIN, g.getWidth(), g.getHeight()-MARGIN-1
);
};
this.clearCell = function(r, c) {
if (!r && !c) {
g.setColor("#ffff00");
@ -243,7 +255,7 @@ let mazeMenu = {
"< Exit": function() { setTimeout(load, 100); } // timeout voodoo prevents deadlock
};
g.clear(true);
g.reset();
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setLocked(false);
@ -253,7 +265,6 @@ let maze_interval = setInterval(
function() {
if (maze) {
if (digitalRead(BTN1) || maze.status==STATUS_ABORTED) {
console.log(`aborting ${start_time}`);
maze = null;
start_time = duration = 0;
aborting = false;
@ -270,7 +281,7 @@ let maze_interval = setInterval(
duration = Date.now()-start_time;
g.setFontAlign(0,0).setColor(g.theme.fg);
g.setFont("Vector",18);
g.drawString(`Solved in\n ${timeToText(duration)} \nClick to play again`, g.getWidth()/2, g.getHeight()/2, true);
g.drawString(`Solved ${maze.n}X${maze.n} in\n ${timeToText(duration)} \nBtn1 to play again`, g.getWidth()/2, g.getHeight()/2, true);
}
}
}, 25);

15
apps/acmaze/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{ "id": "acmaze",
"name": "AccelaMaze",
"shortName":"AccelaMaze",
"version":"0.03",
"description": "Tilt the watch to roll a ball through a maze.",
"icon": "app.png",
"tags": "game",
"supports" : ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"screenshots": [{"url":"screenshot.png"}],
"storage": [
{"name":"acmaze.app.js","url":"app.js"},
{"name":"acmaze.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,18 @@
{
"id": "activepedom",
"name": "Active Pedometer",
"shortName": "Active Pedometer",
"version": "0.09",
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
"icon": "app.png",
"tags": "outdoors,widget",
"supports": ["BANGLEJS"],
"readme": "README.md",
"screenshots": [{"url":"600.png"},{"url":"10600.png"},{"url":"1600.png"}],
"storage": [
{"name":"activepedom.wid.js","url":"widget.js"},
{"name":"activepedom.settings.js","url":"settings.js"},
{"name":"activepedom.img","url":"app-icon.js","evaluate":true},
{"name":"activepedom.app.js","url":"app.js"}
]
}

18
apps/alarm/metadata.json Normal file
View File

@ -0,0 +1,18 @@
{
"id": "alarm",
"name": "Default Alarm & Timer",
"shortName": "Alarms",
"version": "0.14",
"description": "Set and respond to alarms and timers",
"icon": "app.png",
"tags": "tool,alarm,widget",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"alarm.app.js","url":"app.js"},
{"name":"alarm.boot.js","url":"boot.js"},
{"name":"alarm.js","url":"alarm.js"},
{"name":"alarm.img","url":"app-icon.js","evaluate":true},
{"name":"alarm.wid.js","url":"widget.js"}
],
"data": [{"name":"alarm.json"}]
}

View File

@ -0,0 +1,14 @@
{
"id": "alpinenav",
"name": "Alpine Nav",
"version": "0.01",
"description": "App that performs GPS monitoring to track and display position relative to a given origin in realtime",
"icon": "app-icon.png",
"tags": "outdoors,gps",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"alpinenav.app.js","url":"app.js"},
{"name":"alpinenav.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "analogimgclk",
"name": "Analog Clock (Image background)",
"shortName": "Analog Clock",
"version": "0.03",
"description": "An analog clock with an image background",
"icon": "app.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"storage": [
{"name":"analogimgclk.app.js","url":"app.js"},
{"name":"analogimgclk.bg.img","url":"bg.img"},
{"name":"analogimgclk.img","url":"app-icon.js","evaluate":true}
]
}

15
apps/andark/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{ "id": "andark",
"name": "Analog Dark",
"shortName":"AnDark",
"version":"0.04",
"description": "analog clock face without disturbing widgets",
"icon": "andark_icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"andark.app.js","url":"app.js"},
{"name":"andark.img","url":"app_icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,20 @@
{
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.06",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",
"dependencies": {"messages":"app"},
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"android.app.js","url":"app.js"},
{"name":"android.settings.js","url":"settings.js"},
{"name":"android.img","url":"app-icon.js","evaluate":true},
{"name":"android.boot.js","url":"boot.js"}
],
"data": [{"name":"android.settings.json"}],
"sortorder": -8
}

View File

@ -0,0 +1,21 @@
{
"id": "animals",
"name": "Animals Game",
"version": "0.01",
"description": "Simple toddler's game - displays a different number of animals each time the screen is pressed",
"icon": "animals.png",
"tags": "game",
"supports": ["BANGLEJS"],
"storage": [
{"name":"animals.app.js","url":"animals.js"},
{"name":"animals.img","url":"animals-icon.js","evaluate":true},
{"name":"animals-snake.img","url":"animals-snake.js","evaluate":true},
{"name":"animals-duck.img","url":"animals-duck.js","evaluate":true},
{"name":"animals-swan.img","url":"animals-swan.js","evaluate":true},
{"name":"animals-fox.img","url":"animals-fox.js","evaluate":true},
{"name":"animals-camel.img","url":"animals-camel.js","evaluate":true},
{"name":"animals-pig.img","url":"animals-pig.js","evaluate":true},
{"name":"animals-sheep.img","url":"animals-sheep.js","evaluate":true},
{"name":"animals-mouse.img","url":"animals-mouse.js","evaluate":true}
]
}

View File

@ -0,0 +1,18 @@
{
"id": "animclk",
"name": "Animated Clock",
"shortName": "Anim Clock",
"version": "0.03",
"description": "An animated clock face using Mark Ferrari's amazing 8 bit game art and palette cycling: http://www.markferrari.com/art/8bit-game-art",
"icon": "app.png",
"type": "clock",
"tags": "clock,animated",
"supports": ["BANGLEJS"],
"storage": [
{"name":"animclk.app.js","url":"app.js"},
{"name":"animclk.pixels1","url":"animclk.pixels1"},
{"name":"animclk.pixels2","url":"animclk.pixels2"},
{"name":"animclk.pal","url":"animclk.pal"},
{"name":"animclk.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,19 @@
{
"id": "antonclk",
"name": "Anton Clock",
"version": "0.06",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"antonclk.app.js","url":"app.js"},
{"name":"antonclk.settings.js","url":"settings.js"},
{"name":"antonclk.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"antonclk.json"}]
}

15
apps/arrow/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{
"id": "arrow",
"name": "Arrow Compass",
"version": "0.05",
"description": "Moving arrow compass that points North, shows heading, with tilt correction. Based on jeffmer's Navigation Compass",
"icon": "arrow.png",
"type": "app",
"tags": "tool,outdoors",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"arrow.app.js","url":"app.js"},
{"name":"arrow.img","url":"icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,13 @@
{
"id": "assistedgps",
"name": "Assisted GPS Update (AGPS)",
"version": "0.03",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"icon": "app.png",
"type": "RAM",
"tags": "tool,outdoors,agps",
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,
"storage": []
}

15
apps/astral/metadata.json Normal file
View File

@ -0,0 +1,15 @@
{
"id": "astral",
"name": "Astral Clock",
"version": "0.03",
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"astral.app.js","url":"app.js"},
{"name":"astral.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,23 @@
{
"id": "astrocalc",
"name": "Astrocalc",
"version": "0.02",
"description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.",
"icon": "astrocalc.png",
"tags": "app,sun,moon,cycles,tool,outdoors",
"supports": ["BANGLEJS"],
"allow_emulator": true,
"storage": [
{"name":"astrocalc.app.js","url":"astrocalc-app.js"},
{"name":"suncalc.js","url":"suncalc.js"},
{"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true},
{"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true},
{"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true},
{"name":"waning-crescent.img","url":"waning-crescent-icon.js","evaluate":true},
{"name":"waning-gibbous.img","url":"waning-gibbous-icon.js","evaluate":true},
{"name":"full.img","url":"full-icon.js","evaluate":true},
{"name":"new.img","url":"new-icon.js","evaluate":true},
{"name":"waxing-gibbous.img","url":"waxing-gibbous-icon.js","evaluate":true},
{"name":"waxing-crescent.img","url":"waxing-crescent-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,15 @@
{
"id": "astroid",
"name": "Asteroids!",
"version": "0.03",
"description": "Retro asteroids game",
"icon": "asteroids.png",
"screenshots": [{"url":"screenshot_asteroids.png"}],
"tags": "game",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"astroid.app.js","url":"asteroids.js"},
{"name":"astroid.img","url":"asteroids-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,19 @@
{
"id": "authentiwatch",
"name": "2FA Authenticator",
"shortName": "AuthWatch",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"version": "0.04",
"description": "Google Authenticator compatible tool.",
"tags": "tool",
"interface": "interface.html",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"authentiwatch.app.js","url":"app.js"},
{"name":"authentiwatch.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"authentiwatch.json"}]
}

View File

@ -0,0 +1,17 @@
{
"id":"awairmonitor",
"name":"Awair Monitor",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"allow_emulator": true,
"version":"0.03",
"description": "Displays the level of CO2, VOC, PM 2.5, Humidity and Temperature, from your Awair device.",
"type": "clock",
"tags": "clock,tool,health",
"readme":"README.md",
"supports":["BANGLEJS2"],
"storage": [
{"name":"awairmonitor.app.js","url":"app.js"},
{"name":"awairmonitor.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -1,4 +1,5 @@
(() => {
BANGLEJS2 = process.env.HWVERSION==2;
Bangle.setLCDTimeout(0);
let intervalID;
let settings = require("Storage").readJSON("ballmaze.json",true) || {};
@ -6,7 +7,9 @@
// density, elasticity of bounces, "drag coefficient"
const rho = 100, e = 0.3, C = 0.01;
// screen width & height in pixels
const sW = 240, sH = 160;
const sW = g.getWidth();
const sH = g.getHeight()*2/3;
const bgColour ="#f00"; // only for Bangle.js 2
// gravity constant (lowercase was already taken)
const G = 9.80665;
@ -17,14 +20,16 @@
// The play area is 240x160, sizes are the ball radius, so we can use common
// denominators of 120x80 to get square rooms
// Reverse the order to show the easiest on top of the menu
const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(),
// even size 1 actually works, but larger mazes take forever to generate
minSize = 4, defaultSize = 10;
const sizeNames = {
1: "Insane", 2: "Gigantic", 4: "Enormous", 5: "Huge", 8: "Large",
10: "Medium", 16: "Small", 20: "Tiny", 40: "Trivial",
};
// even size 1 actually works, but larger mazes take forever to generate
if (!BANGLEJS2) {
const sizes = [1, 2, 4, 5, 8, 10, 16, 20, 40].reverse(), minSize = 4, defaultSize = 10;
} else {
const sizes = [1, 2, 4, 5, 8, 10, 16, 20 ].reverse(), minSize = 4, defaultSize = 10;
}
/**
* Draw something to all screen buffers
* @param draw {function} Callback which performs the drawing
@ -45,17 +50,17 @@
// use unbuffered graphics for UI stuff
function showMessage(message, title) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showMessage(message, title);
}
function showPrompt(prompt, options) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showPrompt(prompt, options);
}
function showMenu(menu) {
Bangle.setLCDMode();
if (!BANGLEJS2) Bangle.setLCDMode();
return E.showMenu(menu);
}
@ -105,7 +110,7 @@
generateMaze(); // this shows unbuffered progress messages
if (settings.cheat && r>1) findRoute(); // not enough memory for r==1 :-(
Bangle.setLCDMode("doublebuffered");
if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered");
clearAll();
drawAll(drawMaze);
intervalID = setInterval(tick, 100);
@ -307,6 +312,7 @@
const range = {top: 0, left: 0, bottom: rows, right: cols};
const w = sW/cols, h = sH/rows;
g.clear();
if (BANGLEJS2) g.setBgColor(bgColour);
g.setColor(0.76, 0.60, 0.42);
for(let row = range.top; row<=range.bottom; row++) {
for(let col = range.left; col<=range.right; col++) {

View File

@ -0,0 +1,16 @@
{
"id": "ballmaze",
"name": "Ball Maze",
"version": "0.02",
"description": "Navigate a ball through a maze by tilting your watch.",
"icon": "icon.png",
"type": "app",
"tags": "game",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"ballmaze.app.js","url":"app.js"},
{"name":"ballmaze.img","url":"icon.js","evaluate":true}
],
"data": [{"name":"ballmaze.json"}]
}

View File

@ -1,2 +1,3 @@
0.01: Initial version of Balltastic released! Happy!
0.02: Set LCD timeout for Espruino 2v10 compatibility
0.03: Now also works on Bangle.js 2

View File

@ -1,11 +1,12 @@
BANGLEJS2 = process.env.HWVERSION==2;
Bangle.setLCDBrightness(1);
Bangle.setLCDMode("doublebuffered");
if (!BANGLEJS2) Bangle.setLCDMode("doublebuffered");
Bangle.setLCDTimeout(0);
let points = 0;
let level = 1;
let levelSpeedStart = 0.8;
let nextLevelPoints = 20;
let nextLevelPoints = 10;
let levelSpeedFactor = 0.2;
let counterWidth = 10;
let gWidth = g.getWidth() - counterWidth;
@ -81,12 +82,23 @@ function drawLevelText() {
g.setColor("#26b6c7");
g.setFontAlign(0, 0);
g.setFont("4x6", 5);
g.drawString("Level " + level, 120, 80);
g.drawString("Level " + level, g.getWidth()/2, g.getHeight()/2);
}
function drawPointsText() {
g.setColor("#26b6c7");
g.setFontAlign(0, 0);
g.setFont("4x6", 2);
g.drawString("Points " + points, g.getWidth()/2, g.getHeight()-20);
}
function draw() {
//bg
if (!BANGLEJS2) {
g.setColor("#71c6cf");
} else {
g.setColor("#002000");
}
g.fillRect(0, 0, g.getWidth(), g.getHeight());
//counter
@ -94,6 +106,7 @@ function draw() {
//draw level
drawLevelText();
drawPointsText();
//dot
g.setColor("#ff0000");
@ -152,7 +165,7 @@ function count() {
if (counter <= 0) {
running = false;
clearInterval(drawInterval);
setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Gameover!");},50);
setTimeout(function(){ E.showMessage("Press Button 1\nto restart.", "Game over!");},50);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,15 @@
{
"id": "balltastic",
"name": "Balltastic",
"version": "0.03",
"description": "Simple but fun ball eats dots game.",
"icon": "app.png",
"screenshots": [{"url":"bangle2-balltastic-screenshot.png"}],
"type": "app",
"tags": "game,fun",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"balltastic.app.js","url":"app.js"},
{"name":"balltastic.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "banglebridge",
"name": "BangleBridge",
"shortName": "BangleBridge",
"version": "0.01",
"description": "Widget that allows Bangle Js to record pair and end data using Bluetooth Low Energy in combination with the BangleBridge Android App",
"icon": "widget.png",
"type": "widget",
"tags": "widget",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"banglebridge.wid.js","url":"widget.js"},
{"name":"banglebridge.watch.img","url":"watch.img"},
{"name":"banglebridge.heart.img","url":"heart.img"}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "banglerun",
"name": "BangleRun",
"shortName": "BangleRun",
"version": "0.10",
"description": "An app for running sessions. Displays info and logs your run for later viewing.",
"icon": "banglerun.png",
"tags": "run,running,fitness,outdoors",
"supports": ["BANGLEJS"],
"interface": "interface.html",
"allow_emulator": false,
"storage": [
{"name":"banglerun.app.js","url":"app.js"},
{"name":"banglerun.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,21 @@
{ "id": "banglexercise",
"name": "BanglExercise",
"shortName":"BanglExercise",
"version":"0.02",
"description": "Can automatically track exercises while wearing the Bangle.js watch.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "app",
"tags": "sport",
"supports" : ["BANGLEJS2"],
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"banglexercise.app.js","url":"app.js"},
{"name":"banglexercise.img","url":"app-icon.js","evaluate":true},
{"name":"banglexercise.settings.js","url":"settings.js"}
],
"data": [
{"name":"banglexercise.json"}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "barclock",
"name": "Bar Clock",
"version": "0.09",
"description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"barclock.app.js","url":"clock-bar.js"},
{"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "batchart",
"name": "Battery Chart",
"shortName": "Battery Chart",
"version": "0.10",
"description": "A widget and an app for recording and visualizing battery percentage over time.",
"icon": "app.png",
"tags": "app,widget,battery,time,record,chart,tool",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"batchart.wid.js","url":"widget.js"},
{"name":"batchart.app.js","url":"app.js"},
{"name":"batchart.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "batclock",
"name": "Bat Clock",
"shortName": "Bat Clock",
"version": "0.02",
"description": "Morphing Clock, with an awesome \"The Dark Knight\" themed logo.",
"icon": "bat-clock.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"batclock.app.js","url":"bat-clock.app.js"},
{"name":"batclock.img","url":"bat-clock.icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "battleship",
"name": "Battleship",
"version": "0.01",
"description": "The classic game of battleship",
"icon": "battleship-icon.png",
"tags": "game",
"supports": ["BANGLEJS"],
"screenshots": [{"url":"bangle1-battle-ship-screenshot.png"}],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"battleship.app.js","url":"battleship.js"},
{"name":"battleship.img","url":"battleship-icon.js","evaluate":true}
]
}

16
apps/bclock/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"id": "bclock",
"name": "Binary Clock",
"version": "0.03",
"description": "A simple binary clock watch face",
"icon": "clock-binary.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS"],
"allow_emulator": true,
"screenshots": [{"url":"bangle1-binary-clock-screenshot.png"}],
"storage": [
{"name":"bclock.app.js","url":"clock-binary.js"},
{"name":"bclock.img","url":"clock-binary-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "beebclock",
"name": "Beeb Clock",
"version": "0.05",
"description": "Clock face that may be coincidentally familiar to BBC viewers",
"icon": "beebclock.png",
"type": "clock",
"tags": "clock",
"screenshots": [{"url":"bangle1-beeb-clock-screenshot.png"}],
"supports": ["BANGLEJS"],
"allow_emulator": true,
"storage": [
{"name":"beebclock.app.js","url":"beebclock.js"},
{"name":"beebclock.img","url":"beebclock-icon.js","evaluate":true}
]
}

14
apps/beer/metadata.json Normal file
View File

@ -0,0 +1,14 @@
{
"id": "beer",
"name": "Beer Compass",
"version": "0.01",
"description": "Uploads all the pubs in an area onto your watch, so it can always point you at the nearest one",
"icon": "app.png",
"tags": "",
"supports": ["BANGLEJS"],
"custom": "custom.html",
"storage": [
{"name":"beer.app.js"},
{"name":"beer.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "berlinc",
"name": "Berlin Clock",
"version": "0.05",
"description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)",
"icon": "berlin-clock.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"berlin-clock-screenshot.png"}],
"storage": [
{"name":"berlinc.app.js","url":"berlin-clock.js"},
{"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,15 @@
{
"id": "binclock",
"name": "Binary Clock",
"shortName": "Binary Clock",
"version": "0.03",
"description": "A binary clock with hours and minutes. BTN1 toggles a digital clock.",
"icon": "app.png",
"type": "clock",
"tags": "clock,binary",
"supports": ["BANGLEJS"],
"storage": [
{"name":"binclock.app.js","url":"app.js"},
{"name":"binclock.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,19 @@
{ "id": "binwatch",
"name": "Binary Watch",
"shortName":"BinWatch",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.04",
"supports": ["BANGLEJS2"],
"readme": "README.md",
"allow_emulator":true,
"description": "Famous binary watch",
"tags": "clock",
"type": "clock",
"storage": [
{"name":"binwatch.app.js","url":"app.js"},
{"name":"binwatch.bg176.img","url":"Background176_center.img"},
{"name":"binwatch.bg240.img","url":"Background240_center.img"},
{"name":"binwatch.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "blackjack",
"name": "Black Jack game",
"shortName": "Black Jack game",
"version": "0.02",
"description": "Simple implementation of card game Black Jack",
"icon": "blackjack.png",
"tags": "game",
"supports": ["BANGLEJS"],
"screenshots": [{"url":"bangle1-black-jack-game-screenshot.png"}],
"allow_emulator": true,
"storage": [
{"name":"blackjack.app.js","url":"blackjack.app.js"},
{"name":"blackjack.img","url":"blackjack-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,15 @@
{
"id": "bledetect",
"name": "BLE Detector",
"shortName": "BLE Detector",
"version": "0.03",
"description": "Detect BLE devices and show some informations.",
"icon": "bledetect.png",
"tags": "app,bluetooth,tool",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"bledetect.app.js","url":"bledetect.js"},
{"name":"bledetect.img","url":"bledetect-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,13 @@
{
"id": "blescan",
"name": "BLE Scanner",
"version": "0.01",
"description": "Scan for advertising BLE devices",
"icon": "blescan.png",
"tags": "bluetooth",
"supports": ["BANGLEJS"],
"storage": [
{"name":"blescan.app.js","url":"blescan.js"},
{"name":"blescan.img","url":"blescan-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "blobclk",
"name": "Large Digit Blob Clock",
"shortName": "Blob Clock",
"version": "0.06",
"description": "A clock with big digits",
"icon": "clock-blob.png",
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"bangle2-large-digit-blob-clock-screenshot.png"},{"url":"bangle1-large-digit-blob-clock-screenshot.png"}],
"storage": [
{"name":"blobclk.app.js","url":"clock-blob.js"},
{"name":"blobclk.img","url":"clock-blob-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "bluetoothdock",
"name": "Bluetooth Dock",
"shortName": "Dock",
"version": "0.01",
"description": "When charging shows the time, scans Bluetooth for known devices (eg temperature) and shows them on the screen",
"icon": "app.png",
"tags": "bluetooth",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"bluetoothdock.app.js","url":"app.js"},
{"name":"bluetoothdock.boot.js","url":"boot.js"},
{"name":"bluetoothdock.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "boldclk",
"name": "Bold Clock",
"version": "0.05",
"description": "Simple, readable and practical clock",
"icon": "bold_clock.png",
"screenshots": [{"url":"screenshot_bold.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"boldclk.app.js","url":"bold_clock.js"},
{"name":"boldclk.img","url":"bold_clock-icon.js","evaluate":true}
]
}

16
apps/boot/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.41",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",
"tags": "tool,system",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":".boot0","url":"boot0.js"},
{"name":".bootcde","url":"bootloader.js"},
{"name":"bootupdate.js","url":"bootupdate.js"}
],
"sortorder": -10
}

View File

@ -0,0 +1,15 @@
{
"id": "bootgattbat",
"name": "BLE GATT Battery Service",
"shortName": "BLE Battery Service",
"version": "0.01",
"description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n",
"icon": "bluetooth.png",
"type": "bootloader",
"tags": "battery,ble,bluetooth,gatt",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"gattbat.boot.js","url":"boot.js"}
]
}

16
apps/breath/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"id": "breath",
"name": "Breathing App",
"shortName": "Breathing App",
"version": "0.01",
"description": "app to aid relaxation and train breath syncronicity using haptics and visualisation, also displays HR",
"icon": "app-icon.png",
"tags": "tools,health",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"breath.app.js","url":"app.js"},
{"name":"breath.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"breath.settings.json","url":"settings.json"}]
}

View File

@ -5,3 +5,5 @@
0.03: Prevent readings from internal sensor mixing into BT values
Mark events with src property
Show actual source of event in app
0.04: Automatically reconnect BT sensor
App buzzes if no BTHRM events for more than 3 seconds

View File

@ -1,13 +1,28 @@
(function() {
var log = function() {};//print
//var sf = require("Storage").open("bthrm.log","a");
var log = function(text, param){
/*var logline = Date.now().toFixed(3) + " - " + text;
if (param){
logline += " " + JSON.stringify(param);
}
sf.write(logline + "\n");
print(logline);*/
}
log("Start");
var blockInit = false;
var gatt;
var status;
var currentRetryTimeout;
var initialRetryTime = 40;
var maxRetryTime = 60000;
var retryTime = initialRetryTime;
var origIsHRMOn = Bangle.isHRMOn;
Bangle.isBTHRMOn = function(){
return (status=="searching" || status=="connecting") || (gatt!==undefined);
}
return (gatt!==undefined && gatt.connected);
};
Bangle.isHRMOn = function() {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
@ -18,16 +33,135 @@
return Bangle.isBTHRMOn();
}
return origIsHRMOn() || Bangle.isBTHRMOn();
};
var serviceFilters = [{
services: [
"180d"
]
}];
function retry(){
log("Retry with time " + retryTime);
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
var clampedTime = retryTime < 200 ? 200 : initialRetryTime;
currentRetryTimeout = setTimeout(() => {
log("Set timeout for retry as " + clampedTime);
initBt();
}, clampedTime);
retryTime = Math.pow(retryTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
}
function onDisconnect(reason) {
log("Disconnect: " + reason);
log("Gatt: ", gatt);
retry();
}
function onCharacteristic(event) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
var dv = event.target.value;
var flags = dv.getUint8(0);
// 0 = 8 or 16 bit
// 1,2 = sensor contact
// 3 = energy expended shown
// 4 = RR interval
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
/* var idx = 2 + (flags&1); // index of next field
if (flags&8) idx += 2; // energy expended
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit(settings.replace ? "HRM" : "BTHRM", {
bpm: bpm,
confidence: bpm == 0 ? 0 : 100,
src: settings.replace ? "bthrm" : undefined
});
}
var reUseCounter=0;
function initBt() {
log("initBt with blockInit: " + blockInit);
if (blockInit){
retry();
return;
}
blockInit = true;
var connectionPromise;
if (reUseCounter > 3){
log("Reuse counter to high")
if (gatt.connected == true){
try {
log("Force disconnect with gatt: ", gatt);
gatt.disconnect();
} catch(e) {
log("Error during force disconnect", e);
}
}
gatt=undefined;
reUseCounter = 0;
}
if (!gatt){
var requestPromise = NRF.requestDevice({ filters: serviceFilters });
connectionPromise = requestPromise.then(function(device) {
gatt = device.gatt;
log("Gatt after request:", gatt);
gatt.device.on('gattserverdisconnected', onDisconnect);
});
} else {
reUseCounter++;
log("Reusing gatt:", gatt);
connectionPromise = gatt.connect();
}
var servicePromise = connectionPromise.then(function() {
return gatt.getPrimaryService(0x180d);
});
var characteristicPromise = servicePromise.then(function(service) {
log("Got service:", service);
return service.getCharacteristic(0x2A37);
});
var notificationPromise = characteristicPromise.then(function(c) {
log("Got characteristic:", c);
c.on('characteristicvaluechanged', onCharacteristic);
return c.startNotifications();
});
notificationPromise.then(()=>{
log("Wait for notifications");
retryTime = initialRetryTime;
blockInit=false;
});
notificationPromise.catch((e) => {
log("Error:", e);
blockInit = false;
retry();
});
}
Bangle.setBTHRMPower = function(isOn, app) {
var settings = require('Storage').readJSON("bthrm.json", true) || {};
// Do app power handling
if (!app) app="?";
log("setBTHRMPower ->", isOn, app);
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
@ -35,63 +169,19 @@
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
log("setBTHRMPower on", app);
if (!Bangle.isBTHRMOn()) {
log("BTHRM not already on");
status = "searching";
NRF.requestDevice({ filters: [{ services: ['180D'] }] }).then(function(device) {
log("Found device "+device.id);
status = "connecting";
device.on('gattserverdisconnected', function(reason) {
gatt = undefined;
});
return device.gatt.connect();
}).then(function(g) {
log("Connected");
gatt = g;
return gatt.getPrimaryService(0x180D);
}).then(function(service) {
return service.getCharacteristic(0x2A37);
}).then(function(characteristic) {
log("Got characteristic");
characteristic.on('characteristicvaluechanged', function(event) {
var dv = event.target.value;
var flags = dv.getUint8(0);
// 0 = 8 or 16 bit
// 1,2 = sensor contact
// 3 = energy expended shown
// 4 = RR interval
var bpm = (flags&1) ? (dv.getUint16(1)/100/* ? */) : dv.getUint8(1); // 8 or 16 bit
/* var idx = 2 + (flags&1); // index of next field
if (flags&8) idx += 2; // energy expended
if (flags&16) {
var interval = dv.getUint16(idx,1); // in milliseconds
}*/
Bangle.emit(settings.replace?"HRM":"BTHRM", {
bpm:bpm,
confidence:100,
src:settings.replace?"bthrm":undefined
});
});
return characteristic.startNotifications();
}).then(function() {
log("Ready");
status = "ok";
}).catch(function(err) {
log("Error",err);
gatt = undefined;
status = "error";
});
initBt();
}
} else { // not on
log("setBTHRMPower off", app);
log("Power off for " + app);
if (gatt) {
log("BTHRM connected - disconnecting");
status = undefined;
try {gatt.disconnect();}catch(e) {
log("BTHRM disconnect error", e);
try {
log("Disconnect with gatt: ", gatt);
gatt.disconnect();
} catch(e) {
log("Error during disconnect", e);
}
blockInit = false;
gatt = undefined;
}
}
@ -100,24 +190,29 @@
var origSetHRMPower = Bangle.setHRMPower;
Bangle.setHRMPower = function(isOn, app) {
log("setHRMPower for " + app + ":" + (isOn?"on":"off"));
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled || !isOn){
log("Enable BTHRM power");
Bangle.setBTHRMPower(isOn, app);
}
if ((settings.enabled && !settings.replace) || !settings.enabled || !isOn){
log("Enable HRM power");
origSetHRMPower(isOn, app);
}
}
var settings = require('Storage').readJSON("bthrm.json", true) || {};
if (settings.enabled && settings.replace){
log("Replace HRM event");
if (!(Bangle._PWR===undefined) && !(Bangle._PWR.HRM===undefined)){
for (var i = 0; i < Bangle._PWR.HRM.length; i++){
var app = Bangle._PWR.HRM[i];
log("Moving app " + app);
origSetHRMPower(0, app);
Bangle.setBTHRMPower(1, app);
if (Bangle._PWR.HRM===undefined) break;
}
}
}
}
})();

View File

@ -10,7 +10,9 @@ function draw(y, event, type, counter) {
g.reset();
g.setFontAlign(0,0);
g.clearRect(0,y,g.getWidth(),y+75);
if (type == null || event == null || counter == 0) return;
if (type == null || event == null || counter == 0){
return;
}
var str = event.bpm + "";
g.setFontVector(40).drawString(str,px,y+20);
str = "Confidence: " + event.confidence;
@ -21,21 +23,27 @@ function draw(y, event, type, counter) {
}
function onBtHrm(e) {
print("Event for BT " + JSON.stringify(e));
counterBt += 5;
//print("Event for BT " + JSON.stringify(e));
if (e.bpm == 0){
Bangle.buzz(100,0.2);
}
if (counterBt == 0){
Bangle.buzz(200,0.5);
}
counterBt += 3;
eventBt = e;
}
function onHrm(e) {
print("Event for Int " + JSON.stringify(e));
counterInt += 5;
//print("Event for Int " + JSON.stringify(e));
counterInt += 3;
eventInt = e;
}
Bangle.on('BTHRM', onBtHrm);
Bangle.on('HRM', onHrm);
Bangle.setHRMPower(1,'bthrm')
Bangle.setHRMPower(1,'bthrm');
g.clear();
Bangle.loadWidgets();
@ -47,13 +55,13 @@ g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16);
function drawInt(){
counterInt--;
if (counterInt < 0) counterInt = 0;
if (counterInt > 5) counterInt = 5;
if (counterInt > 3) counterInt = 3;
draw(24, eventInt, "HRM", counterInt);
}
function drawBt(){
counterBt--;
if (counterBt < 0) counterBt = 0;
if (counterBt > 5) counterBt = 5;
if (counterBt > 3) counterBt = 3;
draw(100, eventBt, "BTHRM", counterBt);
}

19
apps/bthrm/metadata.json Normal file
View File

@ -0,0 +1,19 @@
{
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.04",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",
"tags": "health,bluetooth",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bthrm.app.js","url":"bthrm.js"},
{"name":"bthrm.recorder.js","url":"recorder.js"},
{"name":"bthrm.boot.js","url":"boot.js"},
{"name":"bthrm.img","url":"app-icon.js","evaluate":true},
{"name":"bthrm.settings.js","url":"settings.js"}
]
}

View File

@ -0,0 +1,23 @@
{
"id": "buffgym",
"name": "BuffGym",
"version": "0.02",
"description": "BuffGym is the famous 5x5 workout program for the BangleJS",
"icon": "buffgym.png",
"type": "app",
"tags": "tool,outdoors,gym,exercise",
"supports": ["BANGLEJS"],
"readme": "README.md",
"interface": "buffgym.html",
"allow_emulator": false,
"storage": [
{"name":"buffgym.app.js","url":"buffgym.app.js"},
{"name":"buffgym-set.js","url":"buffgym-set.js"},
{"name":"buffgym-exercise.js","url":"buffgym-exercise.js"},
{"name":"buffgym-workout.js","url":"buffgym-workout.js"},
{"name":"buffgym-workout-a.json","url":"buffgym-workout-a.json"},
{"name":"buffgym-workout-b.json","url":"buffgym-workout-b.json"},
{"name":"buffgym-workout-index.json","url":"buffgym-workout-index.json"},
{"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,15 @@
{
"id": "calculator",
"name": "Calculator",
"shortName": "Calculator",
"version": "0.05",
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
"icon": "calculator.png",
"screenshots": [{"url":"screenshot_calculator.png"}],
"tags": "app,tool",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"calculator.app.js","url":"app.js"},
{"name":"calculator.img","url":"calculator-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,18 @@
{
"id": "calendar",
"name": "Calendar",
"version": "0.06",
"description": "Simple calendar",
"icon": "calendar.png",
"screenshots": [{"url":"screenshot_calendar.png"}],
"tags": "calendar",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"calendar.app.js","url":"calendar.js"},
{"name":"calendar.settings.js","url":"settings.js"},
{"name":"calendar.img","url":"calendar-icon.js","evaluate":true}
],
"data": [{"name":"calendar.json"}]
}

View File

@ -0,0 +1,17 @@
{
"id": "carcrazy",
"name": "Car Crazy",
"shortName": "Car Crazy",
"version": "0.03",
"description": "A simple car game where you try to avoid the other cars by tilting your wrist left and right. Hold down button 2 to start.",
"icon": "carcrash.png",
"tags": "game",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"carcrazy.app.js","url":"app.js"},
{"name":"carcrazy.img","url":"app-icon.js","evaluate":true},
{"name":"carcrazy.settings.js","url":"settings.js"}
],
"data": [{"name":"CarCrazy.csv"}]
}

View File

@ -0,0 +1,16 @@
{
"id": "chargeanim",
"name": "Charge Animation",
"version": "0.02",
"description": "When charging, show a sideways charging animation and keep the screen on. When removed from the charger load the clock again.",
"icon": "icon.png",
"tags": "battery",
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"bangle2-charge-animation-screenshot.png"},{"url":"bangle-charge-animation-screenshot.png"}],
"storage": [
{"name":"chargeanim.app.js","url":"app.js"},
{"name":"chargeanim.boot.js","url":"boot.js"},
{"name":"chargeanim.img","url":"app-icon.js","evaluate":true}
]
}

16
apps/choozi/metadata.json Normal file
View File

@ -0,0 +1,16 @@
{
"id": "choozi",
"name": "Choozi",
"version": "0.01",
"description": "Choose people or things at random using Bangle.js.",
"icon": "app.png",
"tags": "tool",
"supports": ["BANGLEJS"],
"readme": "README.md",
"allow_emulator": true,
"screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}],
"storage": [
{"name":"choozi.app.js","url":"app.js"},
{"name":"choozi.img","url":"app-icon.js","evaluate":true}
]
}

14
apps/chrono/metadata.json Normal file
View File

@ -0,0 +1,14 @@
{
"id": "chrono",
"name": "Chrono",
"shortName": "Chrono",
"version": "0.01",
"description": "Single click BTN1 to add 5 minutes. Single click BTN2 to add 30 seconds. Single click BTN3 to add 5 seconds. Tap to pause or play to timer. Double click BTN1 to reset. When timer finishes the watch vibrates.",
"icon": "chrono.png",
"tags": "tool",
"supports": ["BANGLEJS"],
"storage": [
{"name":"chrono.app.js","url":"chrono.js"},
{"name":"chrono.img","url":"chrono-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "chronowid",
"name": "Chrono Widget",
"shortName": "Chrono Widget",
"version": "0.05",
"description": "Chronometer (timer) which runs as widget.",
"icon": "app.png",
"tags": "tool,widget",
"supports": ["BANGLEJS","BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"chronowid.wid.js","url":"widget.js"},
{"name":"chronowid.app.js","url":"app.js"},
{"name":"chronowid.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -7,3 +7,15 @@
Make circles and text slightly bigger
0.05: Show correct percentage values in circles
Show humidity as weather circle data
0.06: Allow settings empty circles
Support to choose between humidity and wind speed for weather circle progress
Support to show time and progress until next sunrise or sunset
Load daily steps from Bangle health if available
0.07: Allow configuration of minimal heart rate confidence
0.08: Allow configuration of up to 4 circles in a row
0.09: Support to show temperature, air pressure or altitude from internal pressure sensor
Fix sunprogress calculation during night
Refactor settings menu
Colors of circles can be configured
Color depending on value (green -> red, red -> green) option
Good HRM value will not be overwritten so fast anymore

View File

@ -1,26 +1,35 @@
# Circles clock
A clock with circles for different data at the bottom in a probably familiar style
A clock with three or four circles for different data at the bottom in a probably familiar style
By default the time, date and day of week is shown.
It can show the following information (this can be configured):
* Steps (requires [pedometer widget](https://banglejs.com/apps/#pedometer))
* Steps distance (depending on steps)
* Steps
* Steps distance
* Heart rate (automatically updates when screen is on and unlocked)
* Battery (including charging status and battery low warning)
* Weather (requires [weather app](https://banglejs.com/apps/#weather))
* Humidity as circle progress
* Humidity or wind speed as circle progress
* Temperature inside circle
* Condition as icon below circle
* Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation))
* Temperature, air pressure or altitude from internal pressure sensor
The color of each circle can be configured. The following colors are available:
* Basic colors (red, green, blue, yellow, magenta, cyan, black, white)
* Color depending on value (green -> red, red -> green)
## Screenshots
![Screenshot dark theme](screenshot-dark.png)
![Screenshot light theme](screenshot-light.png)
![Screenshot dark theme with four circles](screenshot-dark-4.png)
![Screenshot light theme with four circles](screenshot-light-4.png)
# TODO
* Add sunrise and sunset
* Display moon instead of sun during night on weather circle
## Ideas
* Show compass heading
## Creator
Marco ([myxor](https://github.com/myxor))

View File

@ -1,48 +1,61 @@
const locale = require("locale");
const heatshrink = require("heatshrink");
const storage = require("Storage");
const SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js");
const shoesIcon = heatshrink.decompress(atob("h0OwYJGgmAAgUBkgECgVJB4cSoAUDyEBkARDpADBhMAyQRBgVAkgmDhIUDAAuQAgY1DAAYA="));
const shoesIconGreen = heatshrink.decompress(atob("h0OwYJGhIEDgVIAgUEyQKDkmACgcggVACIeQAYMSgIRCgmApIbDiQUDAAkBkAFDGoYAD"));
const heartIcon = heatshrink.decompress(atob("h0OwYOLkmQhMkgACByVJgESpIFBpEEBAIFBCgIFCCgsABwcAgQOCAAMSpAwDyBNM"));
const powerIcon = heatshrink.decompress(atob("h0OwYQNsAED7AEDmwEDtu2AgUbtuABwXbBIUN23AAoYOCgEDFIgODABI"));
const powerIconGreen = heatshrink.decompress(atob("h0OwYQNkAEDpAEDiQEDkmSAgUJkmABwVJBIUEyVAAoYOCgEBFIgODABI"));
const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSAgUJkmAAoYZDgQpEBwYAJA"));
const shoesIcon = atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA");
const heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA");
const powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA");
const temperatureIcon = atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA");
const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA"));
const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA="));
const weatherPartlyCloudy = heatshrink.decompress(atob("iEQwYQNv0AjgGDn4EDh///gFChwREC4MfxwIBv0//+AC4X4j4FCv/AgfwgED/wIBuAaBBwgFDgP4gf/AAXABwIEBDQQAEA=="));
const weatherRainy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AocAnAFBFIU4EAM//gRBEAIOBhw1C/AmDAosAC4JNIAAg"));
const weatherPartlyRainy = heatshrink.decompress(atob("h0OwYJGjkAnAFCj+AAgU//4FCuEA8EAg8ch/4gEB4////AAoIIBCIMD/wgCg4bBg/8BwMD+AgBh4ZBDQf/FIIABh4IBgAA=="));
const weatherSnowy = heatshrink.decompress(atob("iEQwYROn/8AocH8AECuAFBh0Agf+CIN/4EDx/4j/x4EAgIIBwAXBAogRFDoopFGoxBGABIA="));
const weatherFoggy = heatshrink.decompress(atob("iEQwYROn/8AgUB/EfwAFBh/AgfwgED/wIBuEABwd/4EcDQgFDgE4Fosf///8f//A/Lj/xCQIRNA="));
const weatherStormy = heatshrink.decompress(atob("iEQwYLIg/gAgUB///wAFBh/AgfwgED/wIBuEAj4OCv0AjgaCh/4AoX8gE4AoQpBnAdBF4IRBDQMH/kOHgY7DAo4AOA=="));
const weatherCloudy = atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA");
const weatherSunny = atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA");
const weatherMoon = atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA");
const weatherPartlyCloudy = atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA");
const weatherRainy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA");
const weatherPartlyRainy = atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA");
const weatherSnowy = atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA");
const weatherFoggy = atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA");
const weatherStormy = atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA");
let settings;
const sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA");
const sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA");
function loadSettings() {
settings = storage.readJSON("circlesclock.json", 1) || {
let settings = storage.readJSON("circlesclock.json", 1) || {
'minHR': 40,
'maxHR': 200,
'confidence': 0,
'stepGoal': 10000,
'stepDistanceGoal': 8000,
'stepLength': 0.8,
'batteryWarn': 30,
'showWidgets': false,
'weatherCircleData': 'humidity',
'circleCount': 3,
'circle1': 'hr',
'circle2': 'steps',
'circle3': 'battery'
};
// Load step goal from pedometer widget as fallback
if (settings.stepGoal == undefined) {
const d = require('Storage').readJSON("wpedom.json", 1) || {};
'circle3': 'battery',
'circle4': 'weather'
};
// Load step goal from pedometer widget as fallback
if (settings.stepGoal == undefined) {
const d = storage.readJSON("wpedom.json", 1) || {};
settings.stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000;
}
}
loadSettings();
/*
* Read location from myLocation app
*/
function getLocation() {
return storage.readJSON("mylocation.json", 1) || undefined;
}
let location = getLocation();
const showWidgets = settings.showWidgets || false;
const circleCount = settings.circleCount || 3;
let hrtValue;
let now = Math.round(new Date().getTime() / 1000);
// layout values:
const colorFg = g.theme.dark ? '#fff' : '#000';
@ -53,22 +66,45 @@ const colorGreen = '#008000';
const colorBlue = '#0000ff';
const colorYellow = '#ffff00';
const widgetOffset = showWidgets ? 24 : 0;
const dowOffset = circleCount == 3 ? 22 : 24; // dow offset relative to date
const h = g.getHeight() - widgetOffset;
const w = g.getWidth();
const hOffset = 30 - widgetOffset;
const h1 = Math.round(1 * h / 5 - hOffset);
const h2 = Math.round(3 * h / 5 - hOffset);
const h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position
const circlePosX = [Math.round(w / 6), Math.round(3 * w / 6), Math.round(5 * w / 6)]; // cirle x positions
const radiusOuter = 25;
const radiusInner = 20;
const circleFont = "Vector:15";
const circleFontBig = "Vector:16";
const circleFontSmall = "Vector:13";
/*
* circle x positions
* depending on circleCount
*
* | 1 2 3 4 5 6 |
* | (1) (2) (3) |
* => circles start at 1,3,5 / 6
*
* | 1 2 3 4 5 6 7 8 |
* | (1) (2) (3) (4) |
* => circles start at 1,3,5,7 / 8
*/
const parts = circleCount * 2;
const circlePosX = [
Math.round(1 * w / parts), // circle1
Math.round(3 * w / parts), // circle2
Math.round(5 * w / parts), // circle3
Math.round(7 * w / parts), // circle4
];
const radiusOuter = circleCount == 3 ? 25 : 20;
const radiusInner = circleCount == 3 ? 20 : 15;
const circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10";
const circleFont = circleCount == 3 ? "Vector:15" : "Vector:11";
const circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12";
const iconOffset = circleCount == 3 ? 6 : 8;
const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
function draw() {
g.clear(true);
if (!showWidgets) {
/*
* we are not drawing the widgets as we are taking over the whole screen
@ -86,31 +122,32 @@ function draw() {
}
g.setColor(colorBg);
g.fillRect(0, widgetOffset, w, h);
g.fillRect(0, widgetOffset, w, h2 + 22);
// time
g.setFont("Vector:50");
g.setFontAlign(0, -1);
g.setColor(colorFg);
g.drawString(locale.time(new Date(), 1), w / 2, h1 + 8);
now = Math.round(new Date().getTime() / 1000);
// date & dow
g.setFont("Vector:21");
g.setFontAlign(-1, 0);
g.drawString(locale.date(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2);
g.drawString(locale.dow(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2 + 22);
g.drawString(locale.dow(new Date()), w > 180 ? 2 * w / 10 : w / 10, h2 + dowOffset);
drawCircle(1);
drawCircle(2);
drawCircle(3);
if (circleCount >= 4) drawCircle(4);
}
const defaultCircleTypes = ["steps", "hr", "battery"];
function drawCircle(index) {
let type = settings['circle' + index];
if (!type) type = defaultCircleTypes[index - 1];
const w = getCirclePosition(type);
const w = getCircleXPosition(type);
switch (type) {
case "steps":
drawSteps(w);
@ -127,195 +164,394 @@ function drawCircle(index) {
case "weather":
drawWeather(w);
break;
case "sunprogress":
case "sunProgress":
drawSunProgress(w);
break;
case "temperature":
drawTemperature(w);
break;
case "pressure":
drawPressure(w);
break;
case "altitude":
drawAltitude(w);
break;
case "empty":
// we draw nothing here
return;
}
}
// serves as cache for quicker lookup of circle positions
let circlePositionsCache = [];
/*
* Looks in the following order if a circle with the given type is somewhere visible/configured
* 1. circlePositionsCache
* 2. settings
* 3. defaultCircleTypes
*
* In case 2 and 3 the circlePositionsCache will be updated
*/
function getCirclePosition(type) {
for (let i = 1; i <= 3; i++) {
if (circlePositionsCache[type] >= 0) {
return circlePositionsCache[type];
}
for (let i = 1; i <= circleCount; i++) {
const setting = settings['circle' + i];
if (setting == type) return circlePosX[i - 1];
if (setting == type) {
circlePositionsCache[type] = i - 1;
return i - 1;
}
}
for (let i = 0; i < defaultCircleTypes.length; i++) {
if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) {
return circlePosX[i];
circlePositionsCache[type] = i;
return i;
}
}
return undefined;
}
function getCircleXPosition(type) {
const circlePos = getCirclePosition(type);
if (circlePos != undefined) {
return circlePosX[circlePos];
}
return undefined;
}
function isCircleEnabled(type) {
return getCirclePosition(type) != undefined;
}
function getCircleColor(type) {
const pos = getCirclePosition(type);
const color = settings["circle" + (pos + 1) + "color"];
if (color && color != "") return color;
}
function getCircleIconColor(type, color, percent) {
const pos = getCirclePosition(type);
const colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true;
if (colorizeIcon) {
return getGradientColor(color, percent);
} else {
return "";
}
}
function getGradientColor(color, percent) {
if (isNaN(percent)) percent = 0;
if (percent > 1) percent = 1;
const colorList = [
'#00FF00', '#80FF00', '#FFFF00', '#FF8000', '#FF0000'
];
if (color == "green-red") {
const colorIndex = Math.round(colorList.length * percent);
return colorList[Math.min(colorIndex, colorList.length) - 1] || "#00ff00";
}
if (color == "red-green") {
const colorIndex = colorList.length - Math.round(colorList.length * percent);
return colorList[Math.min(colorIndex, colorList.length)] || "#ff0000";
}
return color;
}
function getImage(graphic, color) {
if (!color || color == "") {
return graphic;
} else {
return {
width: 16,
height: 16,
bpp: 1,
transparent: 0,
buffer: E.toArrayBuffer(graphic),
palette: new Uint16Array([colorBg, g.toColor(color)])
};
}
}
function drawSteps(w) {
if (!w) w = getCirclePosition("steps");
if (!w) w = getCircleXPosition("steps");
const steps = getSteps();
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
drawCircleBackground(w);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
const color = getCircleColor("steps") || colorBlue;
let percent;
const stepGoal = settings.stepGoal || 10000;
if (stepGoal > 0) {
let percent = steps / stepGoal;
percent = steps / stepGoal;
if (stepGoal < steps) percent = 1;
drawGauge(w, h3, percent, colorBlue);
drawGauge(w, h3, percent, color);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
writeCircleText(w, shortValue(steps));
g.setFont(circleFont);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(shortValue(steps), w + 2, h3);
g.drawImage(shoesIcon, w - 6, h3 + radiusOuter - 6);
g.drawImage(getImage(shoesIcon, getCircleIconColor("steps", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
}
function drawStepsDistance(w) {
if (!w) w = getCirclePosition("steps");
if (!w) w = getCircleXPosition("stepsDistance");
const steps = getSteps();
const stepDistance = settings.stepLength || 0.8;
const stepsDistance = Math.round(steps * stepDistance);
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
drawCircleBackground(w);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
const color = getCircleColor("stepsDistance") || colorGreen;
let percent;
const stepDistanceGoal = settings.stepDistanceGoal || 8000;
if (stepDistanceGoal > 0) {
let percent = stepsDistance / stepDistanceGoal;
percent = stepsDistance / stepDistanceGoal;
if (stepDistanceGoal < stepsDistance) percent = 1;
drawGauge(w, h3, percent, colorGreen);
drawGauge(w, h3, percent, color);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
writeCircleText(w, shortValue(stepsDistance));
g.setFont(circleFont);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(shortValue(stepsDistance), w + 2, h3);
g.drawImage(shoesIconGreen, w - 6, h3 + radiusOuter - 6);
g.drawImage(getImage(shoesIcon, getCircleIconColor("stepsDistance", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
}
function drawHeartRate(w) {
if (!w) w = getCirclePosition("hr");
if (!w) w = getCircleXPosition("hr");
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
drawCircleBackground(w);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
const color = getCircleColor("hr") || colorRed;
if (hrtValue != undefined && hrtValue > 0) {
let percent;
if (hrtValue != undefined) {
const minHR = settings.minHR || 40;
const percent = (hrtValue - minHR) / (settings.maxHR - minHR);
drawGauge(w, h3, percent, colorRed);
const maxHR = settings.maxHR || 200;
percent = (hrtValue - minHR) / (maxHR - minHR);
if (isNaN(percent)) percent = 0;
drawGauge(w, h3, percent, color);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
writeCircleText(w, hrtValue != undefined ? hrtValue : "-");
g.setFont(circleFontBig);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(hrtValue != undefined ? hrtValue : "-", w, h3);
g.drawImage(heartIcon, w - 6, h3 + radiusOuter - 6);
g.drawImage(getImage(heartIcon, getCircleIconColor("hr", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
}
function drawBattery(w) {
if (!w) w = getCirclePosition("battery");
if (!w) w = getCircleXPosition("battery");
const battery = E.getBattery();
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
drawCircleBackground(w);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
let color = getCircleColor("battery") || colorYellow;
let percent;
if (battery > 0) {
const percent = battery / 100;
drawGauge(w, h3, percent, colorYellow);
percent = battery / 100;
drawGauge(w, h3, percent, color);
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
g.setFont(circleFont);
g.setFontAlign(0, 0);
let icon = powerIcon;
let color = colorFg;
if (Bangle.isCharging()) {
color = colorGreen;
icon = powerIconGreen;
} else {
if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) {
color = colorRed;
icon = powerIconRed;
}
}
g.setColor(color);
g.drawString(battery + '%', w, h3);
writeCircleText(w, battery + '%');
g.drawImage(icon, w - 6, h3 + radiusOuter - 6);
g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
}
function drawWeather(w) {
if (!w) w = getCirclePosition("weather");
if (!w) w = getCircleXPosition("weather");
const weather = getWeather();
const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
const humidity = weather ? weather.hum : undefined;
const code = weather ? weather.code : -1;
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
drawCircleBackground(w);
const color = getCircleColor("weather") || colorYellow;
let percent;
const data = settings.weatherCircleData || "humidity";
switch (data) {
case "humidity":
const humidity = weather ? weather.hum : undefined;
if (humidity >= 0) {
drawGauge(w, h3, humidity / 100, colorYellow);
percent = humidity / 100;
drawGauge(w, h3, percent, color);
}
break;
case "wind":
if (weather) {
const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
if (wind[1] >= 0) {
if (wind[2] == "kmh") {
wind[1] = windAsBeaufort(wind[1]);
}
// wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
percent = wind[1] / 12;
drawGauge(w, h3, percent, color);
}
}
break;
case "empty":
break;
}
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
drawInnerCircleAndTriangle(w);
g.fillPoly([w, h3, w - 25, h3 + radiusOuter + 5, w + 25, h3 + radiusOuter + 5]);
const content = tempString ? tempString : "?";
g.setFont(content.length < 4 ? circleFont : circleFontSmall);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(content, w, h3);
writeCircleText(w, tempString ? tempString : "?");
if (code > 0) {
const icon = getWeatherIconByCode(code);
if (icon) g.drawImage(icon, w - 6, h3 + radiusOuter - 10);
if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
} else {
g.drawString("?", w, h3 + radiusOuter);
}
}
function drawSunProgress(w) {
if (!w) w = getCircleXPosition("sunprogress");
const percent = getSunProgress();
drawCircleBackground(w);
const color = getCircleColor("sunprogress") || colorYellow;
drawGauge(w, h3, percent, color);
drawInnerCircleAndTriangle(w);
let icon = sunSetDown;
let text = "?";
const times = getSunData();
if (times != undefined) {
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
if (!isDay()) {
// night
if (now > sunRise) {
// after sunRise
const upcomingSunRise = sunRise + 60 * 60 * 24;
text = formatSeconds(upcomingSunRise - now);
} else {
text = formatSeconds(sunRise - now);
}
icon = sunSetUp;
} else {
// day, approx sunrise tomorrow:
text = formatSeconds(sunSet - now);
icon = sunSetDown;
}
}
writeCircleText(w, text);
g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
}
function drawTemperature(w) {
if (!w) w = getCircleXPosition("temperature");
getPressureValue("temperature").then((temperature) => {
drawCircleBackground(w);
const color = getCircleColor("temperature") || colorGreen;
let percent;
if (temperature) {
const min = -40;
const max = 85;
percent = (temperature - min) / (max - min);
drawGauge(w, h3, percent, color);
}
drawInnerCircleAndTriangle(w);
if (temperature)
writeCircleText(w, locale.temp(temperature));
g.drawImage(getImage(temperatureIcon, getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
});
}
function drawPressure(w) {
if (!w) w = getCircleXPosition("pressure");
getPressureValue("pressure").then((pressure) => {
drawCircleBackground(w);
const color = getCircleColor("pressure") || colorGreen;
let percent;
if (pressure && pressure > 0) {
const minPressure = 950;
const maxPressure = 1050;
percent = (pressure - minPressure) / (maxPressure - minPressure);
drawGauge(w, h3, percent, color);
}
drawInnerCircleAndTriangle(w);
if (pressure)
writeCircleText(w, Math.round(pressure));
g.drawImage(getImage(temperatureIcon, getCircleIconColor("pressure", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
});
}
function drawAltitude(w) {
if (!w) w = getCircleXPosition("altitude");
getPressureValue("altitude").then((altitude) => {
drawCircleBackground(w);
const color = getCircleColor("altitude") || colorGreen;
let percent;
if (altitude) {
const min = 0;
const max = 10000;
percent = (altitude - min) / (max - min);
drawGauge(w, h3, percent, color);
}
drawInnerCircleAndTriangle(w);
if (altitude)
writeCircleText(w, locale.distance(Math.round(altitude)));
g.drawImage(getImage(temperatureIcon, getCircleIconColor("altitude", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset);
});
}
/*
* wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale)
*/
function windAsBeaufort(windInKmh) {
const beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118];
let l = 0;
while (l < beaufort.length && beaufort[l] < windInKmh) {
l++;
}
return l;
}
/*
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
@ -342,7 +578,6 @@ function getWeatherIconByCode(code) {
default:
return weatherRainy;
}
break;
case 6:
return weatherSnowy;
case 7:
@ -350,7 +585,7 @@ function getWeatherIconByCode(code) {
case 8:
switch (code) {
case 800:
return weatherSunny;
return isDay() ? weatherSunny : weatherMoon;
case 801:
return weatherPartlyCloudy;
case 802:
@ -358,39 +593,130 @@ function getWeatherIconByCode(code) {
default:
return weatherCloudy;
}
break;
default:
return undefined;
}
}
function isDay() {
const times = getSunData();
if (times == undefined) return true;
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
return (now > sunRise && now < sunSet);
}
function formatSeconds(s) {
if (s > 60 * 60) { // hours
return Math.round(s / (60 * 60)) + "h";
}
if (s > 60) { // minutes
return Math.round(s / 60) + "m";
}
return "<1m";
}
function getSunData() {
if (location != undefined && location.lat != undefined) {
// get today's sunlight times for lat/lon
return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined;
}
return undefined;
}
/*
* Calculated progress of the sun between sunrise and sunset in percent
*
* Taken from rebble app and modified
*/
function getSunProgress() {
const times = getSunData();
if (times == undefined) return 0;
const sunRise = Math.round(times.sunrise.getTime() / 1000);
const sunSet = Math.round(times.sunset.getTime() / 1000);
if (isDay()) {
// during day
const dayLength = sunSet - sunRise;
if (now > sunRise) {
return (now - sunRise) / dayLength;
} else {
return (sunRise - now) / dayLength;
}
} else {
// during night
if (now < sunRise) {
const prevSunSet = sunSet - 60 * 60 * 24;
return 1 - (sunRise - now) / (sunRise - prevSunSet);
} else {
const upcomingSunRise = sunRise + 60 * 60 * 24;
return (upcomingSunRise - now) / (upcomingSunRise - sunSet);
}
}
}
/*
* Draws the background and the grey circle
*/
function drawCircleBackground(w) {
g.clearRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
// Draw rectangle background:
g.setColor(colorBg);
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
// Draw grey background circle:
g.setColor(colorGrey);
g.fillCircle(w, h3, radiusOuter);
}
function drawInnerCircleAndTriangle(w) {
// Draw inner circle
g.setColor(colorBg);
g.fillCircle(w, h3, radiusInner);
// Draw triangle which covers the bottom of the circle
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
}
function radians(a) {
return a * Math.PI / 180;
}
/*
* This draws the actual gauge consisting out of lots of little filled circles
*/
function drawGauge(cx, cy, percent, color) {
const offset = 15;
const end = 345;
const r = radiusInner + 3;
const end = 360 - offset;
const radius = radiusInner + (circleCount == 3 ? 3 : 2);
const size = radiusOuter - radiusInner - 2;
if (percent <= 0) return;
if (percent <= 0) return; // no gauge needed
if (percent > 1) percent = 1;
const startrot = -offset;
const endrot = startrot - ((end - offset) * percent);
const startRotation = -offset;
const endRotation = startRotation - ((end - offset) * percent);
color = getGradientColor(color, percent);
g.setColor(color);
const size = radiusOuter - radiusInner - 2;
// draw gauge
for (let i = startrot; i > endrot - size; i -= size) {
x = cx + r * Math.sin(radians(i));
y = cy + r * Math.cos(radians(i));
for (let i = startRotation; i > endRotation - size; i -= size) {
x = cx + radius * Math.sin(radians(i));
y = cy + radius * Math.cos(radians(i));
g.fillCircle(x, y, size);
}
}
function writeCircleText(w, content) {
if (content == undefined) return;
const font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig;
g.setFont(font);
g.setFontAlign(0, 0);
g.setColor(colorFg);
g.drawString(content, w, h3);
}
function shortValue(v) {
if (isNaN(v)) return '-';
if (v <= 999) return v;
@ -405,6 +731,9 @@ function shortValue(v) {
}
function getSteps() {
if (Bangle.getHealthStatus) {
return Bangle.getHealthStatus("day").steps;
}
if (WIDGETS && WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
}
@ -424,33 +753,67 @@ function enableHRMSensor() {
}
}
let pressureLocked = false;
let pressureCache;
function getPressureValue(type) {
return new Promise((resolve) => {
if (Bangle.getPressure) {
if (!pressureLocked) {
pressureLocked = true;
if (pressureCache && pressureCache[type]) {
resolve(pressureCache[type]);
}
Bangle.getPressure().then(function(d) {
pressureLocked = false;
if (d) {
pressureCache = d;
if (d[type]) {
resolve(d[type]);
}
}
}).catch(() => {});
} else {
if (pressureCache && pressureCache[type]) {
resolve(pressureCache[type]);
}
}
}
});
}
Bangle.on('lock', function(isLocked) {
if (!isLocked) {
draw();
if (isCircleEnabled("hr")) {
enableHRMSensor();
}
draw();
} else {
Bangle.setHRMPower(0, "circleclock");
}
});
let timerHrm;
Bangle.on('HRM', function(hrm) {
if (isCircleEnabled("hr")) {
if (hrm.confidence >= (settings.confidence || 0)) {
hrtValue = hrm.bpm;
if (Bangle.isLCDOn())
if (Bangle.isLCDOn()) {
drawHeartRate();
}
}
// Let us wait before we overwrite "good" HRM values:
if (Bangle.isLCDOn()) {
if (timerHrm) clearTimeout(timerHrm);
timerHrm = setTimeout(() => {
hrtValue = '...';
drawHeartRate();
}, settings.hrmValidity * 1000 || 30000);
}
}
});
Bangle.setUI("clock");
Bangle.loadWidgets();
draw();
setInterval(draw, 60000);
Bangle.on('charging', function(charging) {
if (isCircleEnabled("battery")) drawBattery();
});
@ -458,3 +821,10 @@ Bangle.on('charging', function(charging) {
if (isCircleEnabled("hr")) {
enableHRMSensor();
}
Bangle.setUI("clock");
Bangle.loadWidgets();
draw();
setInterval(draw, 60000);

View File

@ -0,0 +1,21 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.09",
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator":true,
"readme": "README.md",
"storage": [
{"name":"circlesclock.app.js","url":"app.js"},
{"name":"circlesclock.img","url":"app-icon.js","evaluate":true},
{"name":"circlesclock.settings.js","url":"settings.js"}
],
"data": [
{"name":"circlesclock.json"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -6,62 +6,33 @@
settings[key] = value;
storage.write(SETTINGS_FILE, settings);
}
var valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather"];
var namesCircleTypes = ["steps", "distance", "heart", "battery", "weather"];
E.showMenu({
'': { 'title': 'circlesclock' },
'< Back': back,
'min heartrate': {
value: "minHR" in settings ? settings.minHR : 40,
min: 0,
max : 250,
step: 5,
format: x => {
return x;
const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude"];
const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude"];
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000", "green-red", "red-green"];
const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "black", "green->red", "red->green"];
const weatherData = ["empty", "humidity", "wind"];
function showMainMenu() {
let menu ={
'': { 'title': 'Circles clock' },
/*LANG*/'< Back': back,
/*LANG*/'circle count': {
value: "circleCount" in settings ? settings.circleCount : 3,
min: 3,
max : 4,
step: 1,
onchange: x => save('circleCount', x),
},
onchange: x => save('minHR', x),
},
'max heartrate': {
value: "maxHR" in settings ? settings.maxHR : 200,
min: 20,
max : 250,
step: 5,
format: x => {
return x;
},
onchange: x => save('maxHR', x),
},
'step goal': {
value: "stepGoal" in settings ? settings.stepGoal : 10000,
min: 2000,
max : 50000,
step: 2000,
format: x => {
return x;
},
onchange: x => save('stepGoal', x),
},
'step length': {
value: "stepLength" in settings ? settings.stepLength : 0.8,
min: 0.1,
max : 1.5,
step: 0.01,
format: x => {
return x;
},
onchange: x => save('stepLength', x),
},
'step dist goal': {
value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000,
min: 2000,
max : 30000,
step: 1000,
format: x => {
return x;
},
onchange: x => save('stepDistanceGoal', x),
},
'battery warn': {
/*LANG*/'circle 1': ()=>showCircleMenu(1),
/*LANG*/'circle 2': ()=>showCircleMenu(2),
/*LANG*/'circle 3': ()=>showCircleMenu(3),
/*LANG*/'circle 4': ()=>showCircleMenu(4),
/*LANG*/'heartrate': ()=>showHRMenu(),
/*LANG*/'steps': ()=>showStepMenu(),
/*LANG*/'battery warn': {
value: "batteryWarn" in settings ? settings.batteryWarn : 30,
min: 10,
max : 100,
@ -71,28 +42,138 @@
},
onchange: x => save('batteryWarn', x),
},
'show widgets': {
/*LANG*/'show widgets': {
value: "showWidgets" in settings ? settings.showWidgets : false,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: x => save('showWidgets', x),
},
'left': {
value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle1', valuesCircleTypes[x]),
},
'middle': {
value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle2', valuesCircleTypes[x]),
},
'right': {
value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3,
min: 0, max: 4,
format: v => namesCircleTypes[v],
onchange: x => save('circle3', valuesCircleTypes[x]),
/*LANG*/'weather circle': {
value: settings.weatherCircleData ? weatherData.indexOf(settings.weatherCircleData) : 1,
min: 0, max: 2,
format: v => weatherData[v],
onchange: x => save('weatherCircleData', weatherData[x]),
}
});
};
E.showMenu(menu);
}
function showHRMenu() {
let menu = {
'': { 'title': /*LANG*/'Heartrate' },
/*LANG*/'< Back': ()=>showMainMenu(),
/*LANG*/'minimum': {
value: "minHR" in settings ? settings.minHR : 40,
min: 0,
max : 250,
step: 5,
format: x => {
return x + " bpm";
},
onchange: x => save('minHR', x),
},
/*LANG*/'maximum': {
value: "maxHR" in settings ? settings.maxHR : 200,
min: 20,
max : 250,
step: 5,
format: x => {
return x + " bpm";
},
onchange: x => save('maxHR', x),
},
/*LANG*/'min. confidence': {
value: "confidence" in settings ? settings.confidence : 0,
min: 0,
max : 100,
step: 10,
format: x => {
return x + "%";
},
onchange: x => save('confidence', x),
},
/*LANG*/'valid period': {
value: "hrmValidity" in settings ? settings.hrmValidity : 30,
min: 10,
max : 600,
step: 10,
format: x => {
return x + "s";
},
onchange: x => save('hrmValidity', x),
},
};
E.showMenu(menu);
}
function showStepMenu() {
let menu = {
'': { 'title': /*LANG*/'Steps' },
/*LANG*/'< Back': ()=>showMainMenu(),
/*LANG*/'goal': {
value: "stepGoal" in settings ? settings.stepGoal : 10000,
min: 2000,
max : 50000,
step: 2000,
format: x => {
return x;
},
onchange: x => save('stepGoal', x),
},
/*LANG*/'distance goal': {
value: "stepDistanceGoal" in settings ? settings.stepDistanceGoal : 8000,
min: 2000,
max : 30000,
step: 1000,
format: x => {
return x;
},
onchange: x => save('stepDistanceGoal', x),
},
/*LANG*/'step length': {
value: "stepLength" in settings ? settings.stepLength : 0.8,
min: 0.1,
max : 1.5,
step: 0.01,
format: x => {
return x;
},
onchange: x => save('stepLength', x),
}
};
E.showMenu(menu);
}
const defaultCircleTypes = ["steps", "hr", "battery", "weather"];
function showCircleMenu(circleId) {
const circleName = "circle" + circleId;
const colorKey = circleName + "color";
const colorizeIconKey = circleName + "colorizeIcon";
const menu = {
'': { 'title': /*LANG*/'Circle ' + circleId },
/*LANG*/'< Back': ()=>showMainMenu(),
/*LANG*/'data': {
value: settings[circleName]!=undefined ? valuesCircleTypes.indexOf(settings[circleName]) : valuesCircleTypes.indexOf(defaultCircleTypes[circleId -1]),
min: 0, max: valuesCircleTypes.length - 1,
format: v => namesCircleTypes[v],
onchange: x => save(circleName, valuesCircleTypes[x]),
},
/*LANG*/'color': {
value: settings[colorKey] ? valuesColors.indexOf(settings[colorKey]) : 0,
min: 0, max: valuesColors.length - 1,
format: v => namesColors[v],
onchange: x => save(colorKey, valuesColors[x]),
},
/*LANG*/'colorize icon': {
value: colorizeIconKey in settings ? settings[colorizeIconKey] : false,
format: () => (settings[colorizeIconKey] ? 'Yes' : 'No'),
onchange: x => save(colorizeIconKey, x),
},
};
E.showMenu(menu);
}
showMainMenu();
});

View File

@ -0,0 +1,13 @@
{
"id": "clickms",
"name": "Click Master",
"version": "0.01",
"description": "Get several friends to start the game, then compete to see who can press BTN1 the most!",
"icon": "click-master.png",
"tags": "game",
"supports": ["BANGLEJS"],
"storage": [
{"name":"clickms.app.js","url":"click-master.js"},
{"name":"clickms.img","url":"click-master-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "cliclockJS2Enhanced",
"name": "Commandline-Clock JS2 Enhanced",
"shortName": "CLI-Clock JS2",
"version": "0.03",
"description": "Simple CLI-Styled Clock with enhancements. Modes that are hard to use and unneded are removed (BPM, battery info, memory ect) credit to hughbarney for the original code and design. Also added HID media controlls, just swipe on the clock face to controll the media! Gadgetbride support coming soon(hopefully) Thanks to t0m1o1 for media controls!",
"icon": "app.png",
"screenshots": [{"url":"screengrab.png"}],
"type": "clock",
"tags": "clock,cli,command,bash,shell",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"cliclockJS2Enhanced.app.js","url":"app.js"},
{"name":"cliclockJS2Enhanced.img","url":"app.icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,19 @@
{
"id": "clicompleteclk",
"name": "CLI complete clock",
"shortName":"CLI cmplt clock",
"version":"0.03",
"description": "Command line styled clock with lots of information",
"icon": "app.png",
"allow_emulator": true,
"type": "clock",
"tags": "clock,cli,command,bash,shell,weather,hrt",
"supports" : ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"clicompleteclk.app.js","url":"app.js"},
{"name":"clicompleteclk.img","url":"app-icon.js","evaluate":true},
{"name":"clicompleteclk.settings.js","url":"settings.js"}
],
"data": [{"name":"clicompleteclk.json"}]
}

17
apps/cliock/metadata.json Normal file
View File

@ -0,0 +1,17 @@
{
"id": "cliock",
"name": "Commandline-Clock",
"shortName": "CLI-Clock",
"version": "0.15",
"description": "Simple CLI-Styled Clock",
"icon": "app.png",
"screenshots": [{"url":"screenshot_cli.png"}],
"type": "clock",
"tags": "clock,cli,command,bash,shell",
"supports": ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"cliock.app.js","url":"app.js"},
{"name":"cliock.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{
"id": "clock2x3",
"name": "2x3 Pixel Clock",
"version": "0.05",
"description": "This is a simple clock using minimalist 2x3 pixel numerical digits",
"icon": "clock2x3.png",
"screenshots": [{"url":"screenshot_pixel.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"clock2x3.app.js","url":"clock2x3-app.js"},
{"name":"clock2x3.img","url":"clock2x3-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,16 @@
{
"id": "clotris",
"name": "Clock-Tris",
"version": "0.01",
"description": "A fully functional clone of a classic game of falling blocks",
"icon": "clock-tris.png",
"tags": "game",
"supports": ["BANGLEJS"],
"screenshots": [{"url":"bangle1-clock-tris-screenshot.png"}],
"allow_emulator": true,
"storage": [
{"name":"clotris.app.js","url":"clock-tris.js"},
{"name":"clotris.img","url":"clock-tris-icon.js","evaluate":true},
{"name":".trishig","url":"clock-tris-high"}
]
}

View File

@ -0,0 +1,15 @@
{
"id": "color_catalog",
"name": "Colors Catalog",
"shortName": "Colors Catalog",
"version": "0.01",
"description": "Displays RGB565 and RGB888 colors, its name and code in screen.",
"icon": "app.png",
"tags": "Color,input,buttons,touch,UI",
"supports": ["BANGLEJS"],
"readme": "README.md",
"storage": [
{"name":"color_catalog.app.js","url":"app.js"},
{"name":"color_catalog.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{ "id": "colorful_clock",
"name": "Colorful Analog Clock",
"shortName":"Colorful Clock",
"version":"0.03",
"description": "a colorful analog clock",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"colorful_clock.app.js","url":"app.js"},
{"name":"colorful_clock.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,15 @@
{
"id":"colorwheel",
"name":"Color Wheel",
"tags":"app,tool",
"version":"0.01",
"description":"a tappable wheel of good-looking colors",
"readme":"README.md",
"supports":["BANGLEJS2"],
"allow_emulator":true,
"icon":"colorwheel.png",
"storage": [
{"name":"colorwheel.app.js","url":"app.js"},
{"name":"colorwheel.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,14 @@
{
"id": "compass",
"name": "Compass",
"version": "0.05",
"description": "Simple compass that points North",
"icon": "compass.png",
"screenshots": [{"url":"screenshot_compass.png"}],
"tags": "tool,outdoors",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"compass.app.js","url":"compass.js"},
{"name":"compass.img","url":"compass-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,17 @@
{ "id": "configurable_clock",
"name": "Configurable Analog Clock",
"shortName":"Configurable Clock",
"version":"0.02",
"description": "an analog clock with several kinds of faces, hands and colors to choose from",
"icon": "app-icon.png",
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"screenshots": [{"url":"app-screenshot.png"}],
"readme": "README.md",
"storage": [
{"name":"configurable_clock.app.js","url":"app.js"},
{"name":"configurable_clock.img","url":"app-icon.js","evaluate":true}
]
}

Some files were not shown because too many files have changed in this diff Show More