Merge branch 'master' into jekyll-apps.json
12
README.md
|
@ -172,12 +172,13 @@ The widget example is available in [`apps/_example_widget`](apps/_example_widget
|
|||
|
||||
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
|
||||
};
|
||||
|
@ -461,16 +462,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
|
||||
|
|
|
@ -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>
|
||||
|
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Faster maze generation
|
||||
|
|
|
@ -35,21 +35,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 +97,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++) {
|
||||
|
@ -253,7 +273,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 +289,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)} \nClick to play again`, g.getWidth()/2, g.getHeight()/2, true);
|
||||
}
|
||||
}
|
||||
}, 25);
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
{ "id": "acmaze",
|
||||
"name": "AccelaMaze",
|
||||
"shortName":"AccelaMaze",
|
||||
"version":"0.02",
|
||||
"description": "Tilt the watch to roll a ball through a maze.",
|
||||
"icon": "app.png",
|
||||
"tags": "game",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"storage": [
|
||||
{"name":"acmaze.app.js","url":"app.js"},
|
||||
{"name":"acmaze.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
|
@ -7,3 +7,7 @@
|
|||
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
|
||||
|
|
|
@ -5,23 +5,20 @@ A clock with circles for different data at the bottom in a probably familiar sty
|
|||
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))
|
||||
|
||||
## Screenshots
|
||||

|
||||

|
||||
|
||||
# TODO
|
||||
* Add sunrise and sunset
|
||||
* Display moon instead of sun during night on weather circle
|
||||
|
||||
## Creator
|
||||
Marco ([myxor](https://github.com/myxor))
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
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"));
|
||||
|
@ -11,6 +12,7 @@ const powerIconRed = heatshrink.decompress(atob("h0OwYQNoAEDyAEDkgEDpIFDiVJBweSA
|
|||
|
||||
const weatherCloudy = heatshrink.decompress(atob("iEQwYWTgP//+AAoMPAoPwAoN/AocfAgP//0AAgQAB/AFEABgdDAAMDDohMRA"));
|
||||
const weatherSunny = heatshrink.decompress(atob("iEQwYLIg3AAgVgAQMMAo8Am3YAgUB23bAoUNAoIUBjYFCsOwBYoFDDpFgHYI1JI4gFGAAYA="));
|
||||
const weatherMoon = heatshrink.decompress(atob("iEQwIFCgOAh/wj/4n/8AId//wBBBIoRBCoIZBDoI"));
|
||||
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=="));
|
||||
|
@ -18,6 +20,9 @@ const weatherSnowy = heatshrink.decompress(atob("iEQwYROn/8AocH8AECuAFBh0Agf+CIN
|
|||
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 sunSetDown = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wLDg1ggfACoo"));
|
||||
const sunSetUp = heatshrink.decompress(atob("iEQwIHEgOAAocT5EGtEEkF//wRFgfAg1gBIY"));
|
||||
|
||||
let settings;
|
||||
|
||||
function loadSettings() {
|
||||
|
@ -29,6 +34,7 @@ function loadSettings() {
|
|||
'stepLength': 0.8,
|
||||
'batteryWarn': 30,
|
||||
'showWidgets': false,
|
||||
'weatherCircleData': 'humidity',
|
||||
'circle1': 'hr',
|
||||
'circle2': 'steps',
|
||||
'circle3': 'battery'
|
||||
|
@ -40,9 +46,21 @@ function loadSettings() {
|
|||
}
|
||||
}
|
||||
loadSettings();
|
||||
|
||||
|
||||
/*
|
||||
* Read location from myLocation app
|
||||
*/
|
||||
function getLocation() {
|
||||
return storage.readJSON("mylocation.json", 1) || undefined;
|
||||
}
|
||||
let location = getLocation();
|
||||
|
||||
const showWidgets = settings.showWidgets || false;
|
||||
|
||||
let hrtValue;
|
||||
let now = Math.round(new Date().getTime() / 1000);
|
||||
|
||||
|
||||
// layout values:
|
||||
const colorFg = g.theme.dark ? '#fff' : '#000';
|
||||
|
@ -64,7 +82,6 @@ const radiusOuter = 25;
|
|||
const radiusInner = 20;
|
||||
const circleFont = "Vector:15";
|
||||
const circleFontBig = "Vector:16";
|
||||
const circleFontSmall = "Vector:13";
|
||||
|
||||
function draw() {
|
||||
g.clear(true);
|
||||
|
@ -93,6 +110,7 @@ function draw() {
|
|||
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");
|
||||
|
@ -127,16 +145,39 @@ function drawCircle(index) {
|
|||
case "weather":
|
||||
drawWeather(w);
|
||||
break;
|
||||
case "sunprogress":
|
||||
drawSunProgress(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) {
|
||||
if (circlePositionsCache[type] >= 0) {
|
||||
return circlePosX[circlePositionsCache[type]];
|
||||
}
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const setting = settings['circle' + i];
|
||||
if (setting == type) return circlePosX[i - 1];
|
||||
if (setting == type) {
|
||||
circlePositionsCache[type] = i - 1;
|
||||
return circlePosX[i - 1];
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < defaultCircleTypes.length; i++) {
|
||||
if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) {
|
||||
circlePositionsCache[type] = i;
|
||||
return circlePosX[i];
|
||||
}
|
||||
}
|
||||
|
@ -147,16 +188,12 @@ function isCircleEnabled(type) {
|
|||
return getCirclePosition(type) != undefined;
|
||||
}
|
||||
|
||||
|
||||
function drawSteps(w) {
|
||||
if (!w) w = getCirclePosition("steps");
|
||||
const steps = getSteps();
|
||||
|
||||
// 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 stepGoal = settings.stepGoal || 10000;
|
||||
if (stepGoal > 0) {
|
||||
|
@ -165,15 +202,9 @@ function drawSteps(w) {
|
|||
drawGauge(w, h3, percent, colorBlue);
|
||||
}
|
||||
|
||||
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);
|
||||
g.setColor(colorFg);
|
||||
g.drawString(shortValue(steps), w + 2, h3);
|
||||
writeCircleText(w, shortValue(steps));
|
||||
|
||||
g.drawImage(shoesIcon, w - 6, h3 + radiusOuter - 6);
|
||||
}
|
||||
|
@ -184,12 +215,7 @@ function drawStepsDistance(w) {
|
|||
const stepDistance = settings.stepLength || 0.8;
|
||||
const stepsDistance = Math.round(steps * stepDistance);
|
||||
|
||||
// Draw rectangle background:
|
||||
g.setColor(colorBg);
|
||||
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
|
||||
|
||||
g.setColor(colorGrey);
|
||||
g.fillCircle(w, h3, radiusOuter);
|
||||
drawCircleBackground(w);
|
||||
|
||||
const stepDistanceGoal = settings.stepDistanceGoal || 8000;
|
||||
if (stepDistanceGoal > 0) {
|
||||
|
@ -198,15 +224,9 @@ function drawStepsDistance(w) {
|
|||
drawGauge(w, h3, percent, colorGreen);
|
||||
}
|
||||
|
||||
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);
|
||||
g.setColor(colorFg);
|
||||
g.drawString(shortValue(stepsDistance), w + 2, h3);
|
||||
writeCircleText(w, shortValue(stepsDistance));
|
||||
|
||||
g.drawImage(shoesIconGreen, w - 6, h3 + radiusOuter - 6);
|
||||
}
|
||||
|
@ -214,28 +234,18 @@ function drawStepsDistance(w) {
|
|||
function drawHeartRate(w) {
|
||||
if (!w) w = getCirclePosition("hr");
|
||||
|
||||
// Draw rectangle background:
|
||||
g.setColor(colorBg);
|
||||
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
|
||||
drawCircleBackground(w);
|
||||
|
||||
g.setColor(colorGrey);
|
||||
g.fillCircle(w, h3, radiusOuter);
|
||||
|
||||
if (hrtValue != undefined && hrtValue > 0) {
|
||||
if (hrtValue != undefined) {
|
||||
const minHR = settings.minHR || 40;
|
||||
const percent = (hrtValue - minHR) / (settings.maxHR - minHR);
|
||||
const maxHR = settings.maxHR || 200;
|
||||
const percent = (hrtValue - minHR) / (maxHR - minHR);
|
||||
drawGauge(w, h3, percent, colorRed);
|
||||
}
|
||||
|
||||
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(circleFontBig);
|
||||
g.setFontAlign(0, 0);
|
||||
g.setColor(colorFg);
|
||||
g.drawString(hrtValue != undefined ? hrtValue : "-", w, h3);
|
||||
writeCircleText(w, hrtValue != undefined ? hrtValue : "-");
|
||||
|
||||
g.drawImage(heartIcon, w - 6, h3 + radiusOuter - 6);
|
||||
}
|
||||
|
@ -244,25 +254,14 @@ function drawBattery(w) {
|
|||
if (!w) w = getCirclePosition("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);
|
||||
|
||||
g.setColor(colorGrey);
|
||||
g.fillCircle(w, h3, radiusOuter);
|
||||
drawCircleBackground(w);
|
||||
|
||||
if (battery > 0) {
|
||||
const percent = battery / 100;
|
||||
drawGauge(w, h3, percent, colorYellow);
|
||||
}
|
||||
|
||||
g.setColor(colorBg);
|
||||
g.fillCircle(w, h3, radiusInner);
|
||||
|
||||
g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]);
|
||||
|
||||
g.setFont(circleFont);
|
||||
g.setFontAlign(0, 0);
|
||||
drawInnerCircleAndTriangle(w);
|
||||
|
||||
let icon = powerIcon;
|
||||
let color = colorFg;
|
||||
|
@ -275,8 +274,7 @@ function drawBattery(w) {
|
|||
icon = powerIconRed;
|
||||
}
|
||||
}
|
||||
g.setColor(color);
|
||||
g.drawString(battery + '%', w, h3);
|
||||
writeCircleText(w, battery + '%');
|
||||
|
||||
g.drawImage(icon, w - 6, h3 + radiusOuter - 6);
|
||||
}
|
||||
|
@ -285,30 +283,37 @@ function drawWeather(w) {
|
|||
if (!w) w = getCirclePosition("weather");
|
||||
const weather = getWeather();
|
||||
const tempString = weather ? locale.temp(weather.temp - 273.15) : undefined;
|
||||
const humidity = weather ? weather.hum : undefined;
|
||||
const code = weather ? weather.code : -1;
|
||||
|
||||
// Draw rectangle background:
|
||||
g.setColor(colorBg);
|
||||
g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3);
|
||||
|
||||
g.setColor(colorGrey);
|
||||
g.fillCircle(w, h3, radiusOuter);
|
||||
drawCircleBackground(w);
|
||||
|
||||
const data = settings.weatherCircleData || "humidity";
|
||||
switch (data) {
|
||||
case "humidity":
|
||||
const humidity = weather ? weather.hum : undefined;
|
||||
if (humidity >= 0) {
|
||||
drawGauge(w, h3, humidity / 100, colorYellow);
|
||||
}
|
||||
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)
|
||||
drawGauge(w, h3, wind[1] / 12, colorYellow);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
@ -316,6 +321,69 @@ function drawWeather(w) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function drawSunProgress(w) {
|
||||
if (!w) w = getCirclePosition("sunprogress");
|
||||
const percent = getSunProgress();
|
||||
|
||||
drawCircleBackground(w);
|
||||
|
||||
drawGauge(w, h3, percent, colorYellow);
|
||||
|
||||
drawInnerCircleAndTriangle(w);
|
||||
|
||||
let icon = powerIcon;
|
||||
let color = colorFg;
|
||||
if (isDay()) {
|
||||
// day
|
||||
color = colorFg;
|
||||
icon = sunSetDown;
|
||||
} else {
|
||||
// night
|
||||
color = colorGrey;
|
||||
icon = sunSetUp;
|
||||
}
|
||||
g.setColor(color);
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
// day, approx sunrise tomorrow:
|
||||
text = formatSeconds(sunSet - now);
|
||||
}
|
||||
}
|
||||
|
||||
writeCircleText(w, text);
|
||||
|
||||
g.drawImage(icon, w - 6, h3 + radiusOuter - 6);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
|
@ -350,7 +418,7 @@ function getWeatherIconByCode(code) {
|
|||
case 8:
|
||||
switch (code) {
|
||||
case 800:
|
||||
return weatherSunny;
|
||||
return isDay() ? weatherSunny : weatherMoon;
|
||||
case 801:
|
||||
return weatherPartlyCloudy;
|
||||
case 802:
|
||||
|
@ -365,32 +433,122 @@ function getWeatherIconByCode(code) {
|
|||
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.getTimes(new Date(), location.lat, location.lon);
|
||||
}
|
||||
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 (sunSet < sunRise) {
|
||||
const upcomingSunRise = sunRise + 60 * 60 * 24;
|
||||
return 1 - (upcomingSunRise - now) / (upcomingSunRise - sunSet);
|
||||
} else {
|
||||
const lastSunSet = sunSet - 60 * 60 * 24;
|
||||
return (now - lastSunSet) / (sunRise - lastSunSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Draws the background and the grey circle
|
||||
*/
|
||||
function drawCircleBackground(w) {
|
||||
// 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 radius = radiusInner + 3;
|
||||
const size = radiusOuter - radiusInner - 2;
|
||||
|
||||
if (percent <= 0) return;
|
||||
if (percent > 1) percent = 1;
|
||||
|
||||
const startrot = -offset;
|
||||
const endrot = startrot - ((end - offset) * percent);
|
||||
const startRotation = -offset;
|
||||
const endRotation = startRotation - ((end - offset) * 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;
|
||||
g.setFont(content.length < 4 ? circleFontBig : circleFont);
|
||||
|
||||
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 +563,9 @@ function shortValue(v) {
|
|||
}
|
||||
|
||||
function getSteps() {
|
||||
if (Bangle.getHealthStatus) {
|
||||
return Bangle.getHealthStatus("day").steps;
|
||||
}
|
||||
if (WIDGETS && WIDGETS.wpedom !== undefined) {
|
||||
return WIDGETS.wpedom.getSteps();
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
{ "id": "circlesclock",
|
||||
"name": "Circles clock",
|
||||
"shortName":"Circles clock",
|
||||
"version":"0.05",
|
||||
"version":"0.06",
|
||||
"description": "A clock with circles for different data at the bottom in a probably familiar style",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}],
|
||||
"dependencies": {"widpedom":"app"},
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports" : ["BANGLEJS2"],
|
||||
|
|
|
@ -6,8 +6,12 @@
|
|||
settings[key] = value;
|
||||
storage.write(SETTINGS_FILE, settings);
|
||||
}
|
||||
var valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather"];
|
||||
var namesCircleTypes = ["steps", "distance", "heart", "battery", "weather"];
|
||||
|
||||
const valuesCircleTypes = ["steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "empty"];
|
||||
const namesCircleTypes = ["steps", "distance", "heart", "battery", "weather", "sun progress", "empty"];
|
||||
|
||||
const weatherData = ["humidity", "wind", "empty"];
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'circlesclock' },
|
||||
'< Back': back,
|
||||
|
@ -76,21 +80,27 @@
|
|||
format: () => (settings.showWidgets ? 'Yes' : 'No'),
|
||||
onchange: x => save('showWidgets', x),
|
||||
},
|
||||
'weather circle': {
|
||||
value: settings.weatherCircleData ? weatherData.indexOf(settings.weatherCircleData) : 0,
|
||||
min: 0, max: 2,
|
||||
format: v => weatherData[v],
|
||||
onchange: x => save('weatherCircleData', weatherData[x]),
|
||||
},
|
||||
'left': {
|
||||
value: settings.circle1 ? valuesCircleTypes.indexOf(settings.circle1) : 0,
|
||||
min: 0, max: 4,
|
||||
min: 0, max: 6,
|
||||
format: v => namesCircleTypes[v],
|
||||
onchange: x => save('circle1', valuesCircleTypes[x]),
|
||||
},
|
||||
'middle': {
|
||||
value: settings.circle2 ? valuesCircleTypes.indexOf(settings.circle2) : 2,
|
||||
min: 0, max: 4,
|
||||
min: 0, max: 6,
|
||||
format: v => namesCircleTypes[v],
|
||||
onchange: x => save('circle2', valuesCircleTypes[x]),
|
||||
},
|
||||
'right': {
|
||||
value: settings.circle3 ? valuesCircleTypes.indexOf(settings.circle3) : 3,
|
||||
min: 0, max: 4,
|
||||
min: 0, max: 6,
|
||||
format: v => namesCircleTypes[v],
|
||||
onchange: x => save('circle3', valuesCircleTypes[x]),
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,11 @@
|
|||
# Crow Clock
|
||||
|
||||
Crow Clock features the face of Mystery Science Theater's Crow T. Robot.
|
||||
The code is based almost entirely on Bold Clock.
|
||||
|
||||
## Features
|
||||
|
||||
Its got Crow right there on the face! What more do you need?
|
||||
|
||||

|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkG/4AImcikUzBpIAHmURiIXBAYMjCx0hiU/AwfzA4wWIiYJHmMSCxUxgYLKERH/+UU8kvBY/zp1FBZEhp3uDA/yBQURFw8fl3tBoKJEmQWB9vk+MfFw3/n3SEwVCn/zkgGCkni/4wFFwP/mnjkQRB93UpoDBDoM96f/+JUEmKxB+nhl8ypvtolE73kofyj1PPYKSEWAXy8UimckFoXUiUzkUuC4IqBIwogB8gWBJAPd7pGCSAJECJAfxfAXzokSp3t7wvBAYPkkNNB4ZICRoIACokkFYIAE7viogPDkQCBMYkkonuolFqtVqgGCC4hgB+bEEmlNqEFolAAQfUoQPDLgPyVYku8sArx0BAQQGB6TTFOwYABnvVgFEqnkp1FokAhoXEIoPxeYn0CANEggXBoAGCoYQEibCDVAdFqgtBGIPlPII/EC46oBpqIB73tAYPURwioCVIQwFUQIACAoIuFC5AYBpwXD8idEC5fyd4tPC58093tCoIzBRooXCO43/n3u7wXBDQPiC58uI4vkBw0Ta4oABmq5BAAVFqQXITA0wgAAEgSdGj/yTI0lC4tUC44BBL41ACwcEO43xFoMTU47YC8ne8YNFRoTAG+jXMCgUxPAvzFwS/B9qFGIgXyMA00d4Xe6QLFRghgGGAfkFwxDEJAwkBqtVWY3/iQPEJA3zUwIhGUoQADiIXPkIeGGAkzmYXBkczDIZAHGAUyolE7vuqEA73tA4MjH44gCineaYYXBbQlBPo6SCrwQDFYIFD8qMEAA0hroqEAAXkqR8GRYshqotBAAdViQWLJQcVDIVVqJELGQ0ykUikYsJ"))
|
|
@ -0,0 +1,153 @@
|
|||
var img = require("heatshrink").decompress(atob("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"));
|
||||
|
||||
var hour_hand = {
|
||||
width : 61, height : 8, bpp : 1,
|
||||
transparent : 0,
|
||||
buffer : E.toArrayBuffer(atob("/////////////////////////////////////////////////////////////////////////////////w=="))
|
||||
};
|
||||
var minute_hand = {
|
||||
width : 110, height : 4, bpp : 1,
|
||||
transparent : 0,
|
||||
buffer : E.toArrayBuffer(atob("/////////////////////////////////////////////////////////////////////////w=="))
|
||||
};
|
||||
|
||||
//g.fillRect(0,24,239,239); // Apps area
|
||||
let intervalRef = null;
|
||||
const p180 = Math.PI/180;
|
||||
const clock_center = {x:Math.floor((g.getWidth()-1)/2), y:24+Math.floor((g.getHeight()-25)/2)};
|
||||
// ={ x: 119, y: 131 }
|
||||
const radius = Math.floor((g.getWidth()-24+1)/2); // =108
|
||||
|
||||
let tick0 = Graphics.createArrayBuffer(30,8,1,{msb:true});
|
||||
tick0.fillRect(0,0,tick0.getWidth()-1, tick0.getHeight()-1);
|
||||
let tick5 = Graphics.createArrayBuffer(20,6,1,{msb:true});
|
||||
tick5.fillRect(0,0,tick5.getWidth()-1, tick5.getHeight()-1);
|
||||
let tick1 = Graphics.createArrayBuffer(8,4,1,{msb:true});
|
||||
tick1.fillRect(0,0,tick1.getWidth()-1, tick1.getHeight()-1);
|
||||
|
||||
// Adjust hand lengths to be within 'tick' points
|
||||
minute_hand.width=radius-tick1.getWidth()-6;
|
||||
hour_hand.width=radius-tick5.getWidth()-6;
|
||||
|
||||
function big_wheel_x(angle){
|
||||
return clock_center.x + radius * Math.cos(angle*p180);
|
||||
}
|
||||
function big_wheel_y(angle){
|
||||
return clock_center.y + radius * Math.sin(angle*p180);
|
||||
}
|
||||
function rotate_around_x(center_x, angle, tick){
|
||||
return center_x + Math.cos(angle*p180) * tick.getWidth()/2;
|
||||
}
|
||||
function rotate_around_y(center_y, angle, tick){
|
||||
return center_y + Math.sin(angle*p180) * tick.getWidth()/2;
|
||||
}
|
||||
function hour_pos_x(angle){
|
||||
return clock_center.x + Math.cos(angle*p180) * hour_hand.width/2;
|
||||
}
|
||||
function hour_pos_y(angle){
|
||||
return clock_center.y + Math.sin(angle*p180) * hour_hand.width/2;
|
||||
}
|
||||
function minute_pos_x(angle){
|
||||
return clock_center.x + Math.cos(angle*p180) * minute_hand.width/2;
|
||||
}
|
||||
function minute_pos_y(angle){
|
||||
return clock_center.y + Math.sin(angle*p180) * minute_hand.width/2;
|
||||
}
|
||||
function minute_angle(date){
|
||||
//let minutes = date.getMinutes() + date.getSeconds()/60;
|
||||
let minutes = date.getMinutes();
|
||||
return 6*minutes - 90;
|
||||
}
|
||||
function hour_angle(date){
|
||||
let hours= date.getHours() + date.getMinutes()/60;
|
||||
return 30*hours - 90;
|
||||
}
|
||||
|
||||
function draw_clock(){
|
||||
//console.log("draw_clock");
|
||||
let date = new Date();
|
||||
g.reset();
|
||||
g.clearRect(0,24,239,239); // clear app area
|
||||
|
||||
g.drawImage(img, 12, 24);
|
||||
|
||||
// draw cross lines for testing
|
||||
// g.setColor(1,0,0);
|
||||
// g.drawLine(clock_center.x - radius, clock_center.y, clock_center.x + radius, clock_center.y);
|
||||
// g.drawLine(clock_center.x, clock_center.y - radius, clock_center.x, clock_center.y + radius);
|
||||
|
||||
g.setColor(g.theme.fg);
|
||||
let ticks = [0, 90, 180, 270];
|
||||
ticks.forEach((item)=>{
|
||||
let agl = item+180;
|
||||
g.drawImage(tick0.asImage(), rotate_around_x(big_wheel_x(item), agl, tick0), rotate_around_y(big_wheel_y(item), agl, tick0), {rotate:agl*p180});
|
||||
});
|
||||
ticks = [30, 60, 120, 150, 210, 240, 300, 330];
|
||||
ticks.forEach((item)=>{
|
||||
let agl = item+180;
|
||||
g.drawImage(tick5.asImage(), rotate_around_x(big_wheel_x(item), agl, tick5), rotate_around_y(big_wheel_y(item), agl, tick5), {rotate:agl*p180});
|
||||
});
|
||||
|
||||
let hour_agl = hour_angle(date);
|
||||
let minute_agl = minute_angle(date);
|
||||
g.drawImage(hour_hand, hour_pos_x(hour_agl), hour_pos_y(hour_agl), {rotate:hour_agl*p180}); //
|
||||
g.drawImage(minute_hand, minute_pos_x(minute_agl), minute_pos_y(minute_agl), {rotate:minute_agl*p180}); //
|
||||
g.setColor(g.theme.fg);
|
||||
g.fillCircle(clock_center.x, clock_center.y, 6);
|
||||
g.setColor(g.theme.bg);
|
||||
g.fillCircle(clock_center.x, clock_center.y, 3);
|
||||
|
||||
// draw minute ticks. Takes long time to draw!
|
||||
g.setColor(g.theme.fg);
|
||||
for (var i=0; i<60; i++){
|
||||
let agl = i*6+180;
|
||||
g.drawImage(tick1.asImage(), rotate_around_x(big_wheel_x(i*6), agl, tick1), rotate_around_y(big_wheel_y(i*6), agl, tick1), {rotate:agl*p180});
|
||||
}
|
||||
|
||||
g.flip();
|
||||
//console.log(date);
|
||||
}
|
||||
function clearTimers(){
|
||||
//console.log("clearTimers");
|
||||
if(intervalRef) {
|
||||
clearInterval(intervalRef);
|
||||
intervalRef = null;
|
||||
//console.log("interval is cleared");
|
||||
}
|
||||
}
|
||||
function startTimers(){
|
||||
//console.log("startTimers");
|
||||
if(intervalRef) clearTimers();
|
||||
intervalRef = setInterval(draw_clock, 60*1000);
|
||||
//console.log("interval is set");
|
||||
draw_clock();
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower', (on) => {
|
||||
if (on) {
|
||||
//console.log("lcdPower: on");
|
||||
Bangle.drawWidgets();
|
||||
startTimers();
|
||||
} else {
|
||||
//console.log("lcdPower: off");
|
||||
clearTimers();
|
||||
}
|
||||
});
|
||||
Bangle.on('faceUp',function(up){
|
||||
//console.log("faceUp: " + up + " LCD: " + Bangle.isLCDOn());
|
||||
if (up && !Bangle.isLCDOn()) {
|
||||
//console.log("faceUp and LCD off");
|
||||
clearTimers();
|
||||
Bangle.setLCDPower(true);
|
||||
}
|
||||
});
|
||||
|
||||
g.clear();
|
||||
|
||||
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
startTimers();
|
||||
// Show launcher when button pressed
|
||||
Bangle.setUI("clock");
|
After Width: | Height: | Size: 4.3 KiB |
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "crowclk",
|
||||
"name": "Crow Clock",
|
||||
"version": "0.01",
|
||||
"description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face",
|
||||
"icon": "crow_clock.png",
|
||||
"screenshots": [{"url":"screenshot_crow.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"crowclk.app.js","url":"crow_clock.js"},
|
||||
{"name":"crowclk.img","url":"crow_clock-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 4.3 KiB |
|
@ -1 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Set pace format to mm:ss, time format to h:mm:ss,
|
||||
added settings to opt out of GPS and HRM
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
var B2 = process.env.HWVERSION==2;
|
||||
var Layout = require("Layout");
|
||||
var locale = require("locale")
|
||||
var locale = require("locale");
|
||||
var fontHeading = "6x8:2";
|
||||
var fontValue = B2 ? "6x15:2" : "6x8:3";
|
||||
var headingCol = "#888";
|
||||
|
@ -21,21 +21,25 @@ Bangle.drawWidgets();
|
|||
// ---------------------------
|
||||
|
||||
function formatTime(ms) {
|
||||
var s = Math.round(ms/1000);
|
||||
var min = Math.floor(s/60).toString();
|
||||
s = (s%60).toString();
|
||||
return min.padStart(2,0)+":"+s.padStart(2,0);
|
||||
let hrs = Math.floor(ms/3600000).toString();
|
||||
let mins = (Math.floor(ms/60000)%60).toString();
|
||||
let secs = (Math.floor(ms/1000)%60).toString();
|
||||
|
||||
if (hrs === '0')
|
||||
return mins.padStart(2,0)+":"+secs.padStart(2,0);
|
||||
else
|
||||
return hrs+":"+mins.padStart(2,0)+":"+secs.padStart(2,0); // dont pad hours
|
||||
}
|
||||
|
||||
// Format speed in meters/second
|
||||
function formatPace(speed) {
|
||||
if (speed < 0.1667) {
|
||||
return `__'__"`;
|
||||
return `__:__`;
|
||||
}
|
||||
const pace = Math.round(1000 / speed); // seconds for 1km
|
||||
const min = Math.floor(pace / 60); // minutes for 1km
|
||||
const sec = pace % 60;
|
||||
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
|
||||
return ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
|
@ -149,10 +153,12 @@ Bangle.on("step", function(steps) {
|
|||
lastStepCount = steps;
|
||||
});
|
||||
|
||||
let settings = require("Storage").readJSON('run.json',1)||{"use_gps":true,"use_hrm":true};
|
||||
|
||||
// We always call ourselves once a second, if only to update the time
|
||||
setInterval(onTimer, 1000);
|
||||
|
||||
/* Turn GPS and HRM on right at the start to ensure
|
||||
we get the highest chance of a lock. */
|
||||
Bangle.setHRMPower(true,"app");
|
||||
Bangle.setGPSPower(true,"app");
|
||||
if (settings.use_hrm) Bangle.setHRMPower(true,"app");
|
||||
if (settings.use_gps) Bangle.setGPSPower(true,"app");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "run",
|
||||
"name": "Run",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"description": "Displays distance, time, steps, cadence, pace and more for runners.",
|
||||
"icon": "app.png",
|
||||
"tags": "run,running,fitness,outdoors,gps",
|
||||
|
@ -9,6 +9,8 @@
|
|||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"run.app.js","url":"app.js"},
|
||||
{"name":"run.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
{"name":"run.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"run.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [{"name":"run.json"}]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
(function(back) {
|
||||
const SETTINGS_FILE = "run.json";
|
||||
|
||||
// initialize with default settings...
|
||||
let s = {
|
||||
'use_gps': true,
|
||||
'use_hrm': true
|
||||
}
|
||||
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage')
|
||||
let settings = storage.readJSON(SETTINGS_FILE, 1) || {}
|
||||
const saved = settings || {}
|
||||
for (const key in saved) {
|
||||
s[key] = saved[key]
|
||||
}
|
||||
|
||||
function save() {
|
||||
settings = s
|
||||
storage.write(SETTINGS_FILE, settings)
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'Run' },
|
||||
'< Back': back,
|
||||
'Use GPS': {
|
||||
value: s.use_gps,
|
||||
format: () => (s.use_gps ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.use_gps = !s.use_gps;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Use HRM': {
|
||||
value: s.use_hrm,
|
||||
format: () => (s.use_hrm ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.use_hrm = !s.use_hrm;
|
||||
save();
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
|
@ -1 +1,2 @@
|
|||
0.01: Initial release
|
||||
0.02: Don't start drawing with white colour on white canvas
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
(function () {
|
||||
var pen = 'circle';
|
||||
var discard = null;
|
||||
var kule = [255, 255, 255];
|
||||
var kule = [0, 255, 255]; // R, G, B
|
||||
var oldLock = false;
|
||||
|
||||
setInterval(() => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "tinydraw",
|
||||
"name": "TinyDraw",
|
||||
"shortName":"TinyDraw",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"type": "app",
|
||||
"description": "Draw stuff in your wrist",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: App launched
|
||||
0.02: Menu uses the correct screen area and properly resets when closed
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "touchmenu",
|
||||
"name": "TouchMenu",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Redesigned menu that uses the full touchscreen on the Bangle.js 2",
|
||||
"screenshots": [{"url":"touchmenu.gif"}],
|
||||
"icon": "touchmenu.png",
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
E.showMenu = function(items) {
|
||||
const gw = g.getWidth();
|
||||
const gh = g.getHeight();
|
||||
var ar = Bangle.appRect;
|
||||
Bangle.removeAllListeners("drag");
|
||||
if(!items){
|
||||
delete m;
|
||||
g.clearRect(0, 30, gw, gh - 30);
|
||||
g.clearRect(ar.x, ar.y, ar.x2, ar.y2);
|
||||
return false;
|
||||
}
|
||||
var loc = require("locale");
|
||||
|
@ -26,45 +24,45 @@ E.showMenu = function(items) {
|
|||
draw: () => {
|
||||
g.reset().setFont('12x20');
|
||||
m.info.predraw(g);
|
||||
g.setColor(m.info.cB).fillRect(0, 50, gw, gh - 30).setColor(m.info.cF);
|
||||
g.setColor(m.info.cB).fillRect(ar.x, ar.y + 20, ar.x2, ar.y2).setColor(m.info.cF);
|
||||
m.items.forEach((e, i) => {
|
||||
const s = (i * 48) - m.scroll + 50;
|
||||
if(s < 30 || s > gh - 74){
|
||||
const s = (i * 48) - m.scroll + ar.y + 20;
|
||||
if(s < ar.y || s > ar.y2 - 44){
|
||||
return false;
|
||||
}
|
||||
if(i == m.selected){
|
||||
g.setColor(m.info.cHB).fillRect(0, s, gw, Math.min(s + 48, gh - 30)).setColor(m.info.cHF);
|
||||
g.setColor(m.info.cHB).fillRect(ar.x, s, ar.x2, Math.min(s + 48, ar.y2)).setColor(m.info.cHF);
|
||||
}else{
|
||||
g.setColor(m.info.cF);
|
||||
}
|
||||
g.drawString(e.title, (e.icon ? 30 : 10), s + 5);
|
||||
g.drawString(e.title, ar.x + (e.icon ? 30 : 10), s + 5);
|
||||
if(e.icon){
|
||||
g.drawImage(e.icon, 5, s + 5);
|
||||
g.drawImage(e.icon, ar.x + 5, s + 5);
|
||||
}
|
||||
if(e.type && s < gh - 72){
|
||||
if(e.type && s < ar.y2 - 42){
|
||||
if(e.format){
|
||||
g.setFontAlign(1, -1, 0).drawString(e.format(e.value), gw - 10, s + 25).setFontAlign(-1, -1, 0);
|
||||
g.setFontAlign(1, -1, 0).drawString(e.format(e.value), ar.x2 - 10, s + 25).setFontAlign(-1, -1, 0);
|
||||
}else{
|
||||
g.setFontAlign(1, -1, 0).drawString(e.value, gw - 10, s + 25).setFontAlign(-1, -1, 0);
|
||||
g.setFontAlign(1, -1, 0).drawString(e.value, ar.x2 - 10, s + 25).setFontAlign(-1, -1, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
g.setColor(m.info.cAB).fillRect(0, 30, gw, 50);
|
||||
g.setColor(m.info.cAF).drawString(m.info.title, (m.back ? 30 : 10), 32);
|
||||
g.setColor(m.info.cAB).fillRect(ar.x, ar.y, ar.x2, ar.y + 20);
|
||||
g.setColor(m.info.cAF).drawString(m.info.title, ar.x + (m.back ? 30 : 10), ar.y + 2);
|
||||
if(m.back){
|
||||
g.drawLine(5, 40, 20, 40);
|
||||
g.drawLine(5, 40, 15, 33);
|
||||
g.drawLine(5, 40, 15, 47);
|
||||
g.drawLine(ar.x + 5, ar.y + 10, ar.x + 20, ar.y + 10);
|
||||
g.drawLine(ar.x + 5, ar.y + 10, ar.x + 15, ar.y + 17);
|
||||
g.drawLine(ar.x + 5, ar.y + 10, ar.x + 15, ar.y + 3);
|
||||
}
|
||||
m.info.preflip(g, m.scroll > 0, m.scroll < (m.items.length - 1) * 48);
|
||||
},
|
||||
select: (x, y) => {
|
||||
if(m.selected == -1 || m.selected !== Math.max(Math.min(Math.floor((y + m.scroll - 50) / 48), m.items.length - 1), 0)){
|
||||
if(m.selected == -1 || m.selected !== Math.max(Math.min(Math.floor((y + m.scroll - ar.y - 20) / 48), m.items.length - 1), 0)){
|
||||
if(y){
|
||||
if(y < 50 || y > gh - 30){
|
||||
if(y < ar.y + 20 || y > ar.y2){
|
||||
return false;
|
||||
}else{
|
||||
m.selected = Math.max(Math.min(Math.floor((y + m.scroll - 50) / 48), m.items.length - 1), 0);
|
||||
m.selected = Math.max(Math.min(Math.floor((y + m.scroll - ar.y - 20) / 48), m.items.length - 1), 0);
|
||||
}
|
||||
}else{
|
||||
m.selected = Math.floor(m.scroll / 48);
|
||||
|
@ -76,7 +74,7 @@ E.showMenu = function(items) {
|
|||
m.items[m.selected].onchange(m.items[m.selected].value);
|
||||
m.draw();
|
||||
}else if(m.items[m.selected].type && m.items[m.selected].type === "number"){
|
||||
if(x && x < (gw / 2)){
|
||||
if(x && x < ((ar.x + ar.x2) / 2)){
|
||||
m.items[m.selected].value = m.items[m.selected].value - (m.items[m.selected].step ? m.items[m.selected].step : 1);
|
||||
}else{
|
||||
m.items[m.selected].value = m.items[m.selected].value + (m.items[m.selected].step ? m.items[m.selected].step : 1);
|
||||
|
@ -99,7 +97,7 @@ E.showMenu = function(items) {
|
|||
move: d => {
|
||||
m.scroll += (d * 48);
|
||||
m.scroll = Math.min(Math.max(m.scroll, 0), (m.items.length - 1) * 48);
|
||||
m.selected = Math.max(Math.min(Math.floor((m.scroll - 50) / 48), m.items.length - 1), 0);
|
||||
m.selected = Math.max(Math.min(Math.floor((m.scroll - ar.y - 20) / 48), m.items.length - 1), 0);
|
||||
m.draw();
|
||||
},
|
||||
};
|
||||
|
@ -127,7 +125,7 @@ E.showMenu = function(items) {
|
|||
return false;
|
||||
}
|
||||
if(d.dx == 0 && d.dy == 0){
|
||||
if(d.x < 30 && d.y < 50){
|
||||
if(d.x < ar.x + 30 && d.y < ar.y + 20){
|
||||
m.back();
|
||||
return false;
|
||||
}
|
||||
|
@ -195,3 +193,9 @@ E.showPrompt = function (e, t){
|
|||
E.showMenu(menu);
|
||||
});
|
||||
};
|
||||
|
||||
const bsl = Bangle.showLauncher;
|
||||
Bangle.showLauncher = function (){
|
||||
Bangle.removeAllListeners("drag");
|
||||
bsl();
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New watch face
|
||||
0.02: Use Bangle.setUI for button/launcher handling
|
||||
0.03: Bangle.js 2 support
|
||||
0.04: Adds costumizable colours and the respective settings menu
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
const is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
|
||||
const locale = require("locale");
|
||||
var settings = require('Storage').readJSON("vectorclock.json", true) || {};
|
||||
var dowcol = settings.dowcol || g.theme.fg;
|
||||
var timecol = settings.timecol || g.theme.fg;
|
||||
var datecol = settings.datecol || g.theme.fg;
|
||||
|
||||
|
||||
function padNum(n, l) {
|
||||
return ("0".repeat(l)+n).substr(-l);
|
||||
|
@ -26,8 +31,8 @@ function executeCommands() {
|
|||
commands = [];
|
||||
}
|
||||
|
||||
function drawVectorText(text, size, x, y, alignX, alignY) {
|
||||
g.setFont("Vector", size).setFontAlign(alignX, alignY).drawString(text, x, y);
|
||||
function drawVectorText(text, size, x, y, alignX, alignY, color) {
|
||||
g.setFont("Vector", size).setColor(color).setFontAlign(alignX, alignY).drawString(text, x, y);
|
||||
var m = g.stringMetrics(text);
|
||||
return {
|
||||
x1: x - m.width * (alignX / 2 + 0.5),
|
||||
|
@ -64,15 +69,15 @@ function draw() {
|
|||
let x = 2;
|
||||
let y = 24 + spacer;
|
||||
|
||||
pushCommand(drawVectorText, timeText, timeFontSize, x, y, -1, -1);
|
||||
pushCommand(drawVectorText, meridian, timeFontSize*9/20, x + width, y, 1, -1);
|
||||
if (showSeconds) pushCommand(drawVectorText, secondsText, timeFontSize*9/20, x + width, y + timeHeight, 1, 1);
|
||||
pushCommand(drawVectorText, timeText, timeFontSize, x, y, -1, -1, timecol);
|
||||
pushCommand(drawVectorText, meridian, timeFontSize*9/20, x + width, y, 1, -1, timecol);
|
||||
if (showSeconds) pushCommand(drawVectorText, secondsText, timeFontSize*9/20, x + width, y + timeHeight, 1, 1, timecol);
|
||||
y += timeHeight + spacer;
|
||||
|
||||
pushCommand(drawVectorText, dowText, dowFontSize, x + width/2, y, 0, -1);
|
||||
pushCommand(drawVectorText, dowText, dowFontSize, x + width/2, y, 0, -1, dowcol);
|
||||
y += dowHeight + spacer;
|
||||
|
||||
pushCommand(drawVectorText, dateText, dateFontSize, x + width/2, y, 0, -1);
|
||||
pushCommand(drawVectorText, dateText, dateFontSize, x + width/2, y, 0, -1, datecol);
|
||||
|
||||
executeCommands();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
(function(back) {
|
||||
var FILE = "vectorclock.json";
|
||||
// Load settings
|
||||
var settings = Object.assign({
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
var colnames = ["white", "yellow", "green", "cyan", "red", "orange", "magenta", "black"];
|
||||
var colvalues = [0xFFFF, 0xFFE0, 0x07E0, 0x07FF, 0xF800, 0xFD20, 0xF81F, 0x0000];
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "VectorClock colours" },
|
||||
"< Back" : () => back(),
|
||||
'Time': {
|
||||
value: Math.max(0 | colvalues.indexOf(settings.timecol),0),
|
||||
min: 0, max: colvalues.length-1,
|
||||
format: v => colnames[v],
|
||||
onchange: v => {
|
||||
settings.timecol = colvalues[v];
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Weekday': {
|
||||
value: Math.max(0 | colvalues.indexOf(settings.dowcol),0),
|
||||
min: 0, max: colvalues.length-1,
|
||||
format: v => colnames[v],
|
||||
onchange: v => {
|
||||
settings.dowcol = colvalues[v];
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Date': {
|
||||
value: Math.max(0 | colvalues.indexOf(settings.datecol),0),
|
||||
min: 0, max: colvalues.length-1,
|
||||
format: v => colnames[v],
|
||||
onchange: v => {
|
||||
settings.datecol = colvalues[v];
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
0.01: new Widget Editor!
|
|
@ -0,0 +1,16 @@
|
|||
# Widget Editor
|
||||
|
||||
This adds a setting menu which allows you to change the location of widgets.
|
||||
|
||||
## Settings
|
||||
|
||||
There is no app icon in the launcher; you can find the settings under
|
||||
`Apps`->`Widget Editor`.
|
||||
|
||||
For every widget, you have these options:
|
||||
* **Side**: On which side to draw the widget.
|
||||
* **Sort Order**: Changes the order if several widgets use the same side.
|
||||
|
||||
## Creator
|
||||
|
||||
Richard de Boer <rigrig+banglejs@tubul.net>
|
|
@ -0,0 +1,24 @@
|
|||
Bangle.loadWidgets = function() {
|
||||
global.WIDGETS={};
|
||||
require("Storage").list(/\.wid\.js$/)
|
||||
.forEach(w=>{
|
||||
try { eval(require("Storage").read(w)); }
|
||||
catch (e) { print(w, e); }
|
||||
});
|
||||
const s = require("Storage").readJSON("wid_edit.json", 1) || {},
|
||||
c = s.custom || {};
|
||||
for (const w in c){
|
||||
if (!(w in WIDGETS)) continue; // widget no longer exists
|
||||
// store defaults of customized values in _WIDGETS
|
||||
global._WIDGETS=global._WIDGETS||{};
|
||||
_WIDGETS[w] = {};
|
||||
Object.keys(c[w]).forEach(k => _WIDGETS[w][k] = WIDGETS[w][k]);
|
||||
Object.assign(WIDGETS[w], c[w]);
|
||||
}
|
||||
const W = WIDGETS;
|
||||
WIDGETS = {};
|
||||
Object.keys(W)
|
||||
.sort()
|
||||
.sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder))
|
||||
.forEach(k => WIDGETS[k] = W[k]);
|
||||
}
|
After Width: | Height: | Size: 8.1 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "wid_edit",
|
||||
"version": "0.01",
|
||||
"name": "Widget Editor",
|
||||
"icon": "icon.png",
|
||||
"description": "Customize widget locations",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"type": "bootloader",
|
||||
"tags": "widget,tool",
|
||||
"storage": [
|
||||
{"name":"wid_edit.boot.js","url":"boot.js"},
|
||||
{"name":"wid_edit.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"wid_edit.json"}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/**
|
||||
* @param {function} back Use back() to return to settings menu
|
||||
*/
|
||||
(function(back) {
|
||||
const names = {};
|
||||
const settings = require("Storage").readJSON("wid_edit.json", 1) || {};
|
||||
if (!('custom' in settings)) settings.custom = {};
|
||||
global._WIDGETS = global._WIDGETS || {};
|
||||
|
||||
let cleanup = false;
|
||||
for (const id in settings.custom) {
|
||||
if (!(id in WIDGETS)) {
|
||||
// widget which no longer exists
|
||||
cleanup = true;
|
||||
delete settings.custom[id];
|
||||
}
|
||||
}
|
||||
if (cleanup) {
|
||||
if (!Object.keys(settings.custom).length) delete settings.custom;
|
||||
require("Storage").writeJSON("wid_edit.json", settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort & redraw all widgets
|
||||
*/
|
||||
function redrawWidgets() {
|
||||
let W = WIDGETS;
|
||||
global.WIDGETS = {};
|
||||
Object.keys(W)
|
||||
.sort()
|
||||
.sort((a, b) => (0|W[b].sortorder)-(0|W[a].sortorder))
|
||||
.forEach(k => {WIDGETS[k] = W[k]});
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find app name for widget
|
||||
* @param {string} widget WIDGETS key
|
||||
* @return {string} widget name
|
||||
*/
|
||||
function name(widget) {
|
||||
if (!(widget in names)) {
|
||||
let infoFile = widget+".info";
|
||||
// widget names don't always correspond to appid :-(
|
||||
// so we try both with and without 'wid'-prefix
|
||||
if (!require("Storage").list(new RegExp(`^${infoFile}$`)).length) {
|
||||
infoFile = (widget.substr(0, 3)==="wid") ? infoFile.substr(3) : ("wid"+infoFile);
|
||||
}
|
||||
names[widget] = (require("Storage").readJSON(infoFile, 1) || {}).name || widget;
|
||||
}
|
||||
return names[widget];
|
||||
}
|
||||
|
||||
function edit(id) {
|
||||
let WIDGET = WIDGETS[id],
|
||||
def = {area: WIDGET.area, sortorder: WIDGET.sortorder|0}; // default values
|
||||
Object.assign(def, _WIDGETS[id]||{}); // defaults were saved in _WIDGETS
|
||||
|
||||
settings.custom = settings.custom||{};
|
||||
let saved = settings.custom[id] || {},
|
||||
area = saved.area || def.area,
|
||||
sortorder = ("sortorder" in saved) ? saved.sortorder : def.sortorder;
|
||||
|
||||
/**
|
||||
* Draw highlighted widget
|
||||
*/
|
||||
function highlight() {
|
||||
if (WIDGET.width > 0) {
|
||||
// draw widget, then draw a highlighted border on top
|
||||
WIDGET.draw();
|
||||
g.setColor(g.theme.fgH)
|
||||
.drawRect(WIDGET.x, WIDGET.y, WIDGET.x+WIDGET.width-1, WIDGET.y+23);
|
||||
} else {
|
||||
// hidden widget: fake a width and provide our own draw()
|
||||
const draw = WIDGET.draw, width = WIDGET.width;
|
||||
WIDGET.width = 24;
|
||||
WIDGET.draw = function() {
|
||||
g.setColor(g.theme.bgH).setColor(g.theme.fgH)
|
||||
.clearRect(this.x, this.y, this.x+23, this.y+23)
|
||||
.drawRect(this.x, this.y, this.x+23, this.y+23)
|
||||
.drawLine(this.x, this.y, this.x+23, this.y+23)
|
||||
.drawLine(this.x, this.y+23, this.x+23, this.y);
|
||||
};
|
||||
// re-layout+draw all widgets with our placeholder in between
|
||||
redrawWidgets();
|
||||
// and restore original values
|
||||
WIDGET.draw = draw;
|
||||
WIDGET.width = width;
|
||||
}
|
||||
}
|
||||
highlight();
|
||||
|
||||
/**
|
||||
* Save widget and redraw with new settings
|
||||
*/
|
||||
function save() {
|
||||
// we only save non-default values
|
||||
saved = {};
|
||||
if ((area!==def.area) || (sortorder!==def.sortorder)) {
|
||||
if (area!==def.area) saved.area = area;
|
||||
if (sortorder!==def.sortorder) saved.sortorder = sortorder;
|
||||
settings.custom = settings.custom || {};
|
||||
settings.custom[id] = saved;
|
||||
} else if (settings.custom) {
|
||||
delete settings.custom[id]
|
||||
}
|
||||
if (!Object.keys(settings.custom).length) delete settings.custom;
|
||||
require("Storage").writeJSON("wid_edit.json", settings);
|
||||
Object.assign(WIDGET, def, saved);
|
||||
if (WIDGET.sortorder === undefined) delete WIDGET.sortorder; // default can be undefined, but don't put that in the widget
|
||||
// if we assigned custom values, store defaults in _WIDGETS
|
||||
let _W = {};
|
||||
if (saved.area) _W.area = def.area;
|
||||
if ('sortorder' in saved) _W.sortorder = def.sortorder;
|
||||
if (Object.keys(_W).length) _WIDGETS[id] = _W;
|
||||
else delete _WIDGETS[id];
|
||||
|
||||
// drawWidgets won't clear e.g. bottom bar if we just disabled the last bottom widget
|
||||
redrawWidgets();
|
||||
|
||||
highlight();
|
||||
m.draw();
|
||||
}
|
||||
|
||||
const menu = {
|
||||
"": {"title": name(id)},
|
||||
/*LANG*/"< Back": () => {
|
||||
redrawWidgets();
|
||||
mainMenu();
|
||||
},
|
||||
/*LANG*/"Side": {
|
||||
value: (area === 'tl'),
|
||||
format: tl => tl ? /*LANG*/"Left" : /*LANG*/"Right",
|
||||
onchange: tl => {
|
||||
area = tl ? "tl" : "tr";
|
||||
save();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Sort Order": {
|
||||
value: sortorder,
|
||||
onchange: o => {
|
||||
sortorder = o;
|
||||
save();
|
||||
}
|
||||
},
|
||||
/*LANG*/"Reset": () => {
|
||||
area = def.area;
|
||||
sortorder = def.sortorder;
|
||||
save();
|
||||
mainMenu(); // changing multiple values made the rest of the menu wrong, so take the easy out
|
||||
}
|
||||
}
|
||||
|
||||
let m = E.showMenu(menu);
|
||||
}
|
||||
|
||||
|
||||
function mainMenu() {
|
||||
let menu = {
|
||||
"": {"title": /*LANG*/"Widgets"},
|
||||
};
|
||||
menu[/*LANG*/"< Back"] = ()=>{
|
||||
if (!Object.keys(_WIDGETS).length) delete _WIDGETS; // no defaults to remember
|
||||
back();
|
||||
};
|
||||
Object.keys(WIDGETS).forEach(id=>{
|
||||
// mark customized widgets with asterisk
|
||||
menu[name(id)+((id in _WIDGETS) ? " *" : "")] = () => edit(id);
|
||||
});
|
||||
if (Object.keys(_WIDGETS).length) { // only show reset if there is anything to reset
|
||||
menu[/*LANG*/"Reset All"] = () => {
|
||||
E.showPrompt(/*LANG*/"Reset all widgets?").then(confirm => {
|
||||
if (confirm) {
|
||||
delete settings.custom;
|
||||
require("Storage").writeJSON("wid_edit.json", settings);
|
||||
for(let id in _WIDGETS) {
|
||||
Object.assign(WIDGETS[id], _WIDGETS[id]) // restore defaults
|
||||
}
|
||||
global._WIDGETS = {};
|
||||
redrawWidgets();
|
||||
}
|
||||
mainMenu(); // reload with reset widgets
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
mainMenu();
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
0.01: New Widget!
|
||||
0.02: Version using connect back
|
||||
0.03: Version using modified firmware
|
||||
0.04: Works on both standard and modified firmware
|
||||
0.05: Bug fixes w.r.t. reconnection
|
||||
0.06: Update README - Release version
|
||||
0.07: Respect Quiet Mode
|
|
@ -1,70 +0,0 @@
|
|||
## ANCS - iPhone notifications for Bangle.js
|
||||
|
||||
The ANCS widget allows you to answer or cancel iPhone incoming calls and also displays messages and notifications. It connects to the Apple Notification Center Service which is already on all iPhones, so you do not need to install any additional iPhone apps to use this widget.
|
||||
|
||||
## Firmware
|
||||
The widget will run on the standard firmware, however, installation of a slightly modified version - the zip file is available from [this directory](https://github.com/jeffmer/JeffsBangleAppsDev/tree/master/apps/widancs) - will increase the performance of the app by an order of magnitude in terms of the time to connect or reconnect to the iPhone. In addition, the Bangle will stay connected to the iPhone over a greater separation distance than with the standard firmware.
|
||||
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
After the widget is uploaded to the Bangle, it needs to be enabled in the Bangle Settings app:- `ANCS Widget` will appear in `APP/Widget settings`. There is also a menu in these settings to let you configure the categories of notifications that you want to be displayed. You must disconnect from the App Loader before enabling the widget.
|
||||
|
||||
## Compatible Apps
|
||||
|
||||
The widget will only run with a compatible app - for the reason for this see Issue 1 below. The apps that are compatible with the ANCS widget are:- **Multi Clock**, **Navigation Compass** and **GPS Navigation**. When you switch to an app that is not compatible, the ANCS phone icon will not appear.
|
||||
|
||||
## iPhone Pairing
|
||||
Once enabled, the widget icon should be displayed coloured grey (its green in the photo). Go to the phone's Bluetooth settings menu and your Bangle should appear under Other devices. If this is the first time you have connected with the Bangle from your iPhone, it may be named Accessory. Click on the name and the iPhone should connect and start pairing. The widget icon will turn red and the iPhone will ask you to enter a pairing code - the traditional 123456. You have 10 seconds to enter this after which you will need to start pairing again. After that, the iPhone may also ask to allow the device access to ANCS. Once pairing is complete, the widget icon should go blue and eventually green. The range of colours is:
|
||||
|
||||
* **Grey** - not connected - advertising
|
||||
* **Red** - connected - not paired.
|
||||
* **Blue** - paired and connected - getting services
|
||||
* **Yellow** - got Services.
|
||||
* **Green** - waiting for new notifications.
|
||||
|
||||
After pairing the first time, the Bangle should connect automatically when the widget is running. Sometimes you may need you to click on the Bangle name in `Settings:Bluetooth:My devices` on the iPhone or disable and then enable Bluetooth to start connection. If you need to load other apps from the iPhone, it will be necessary to ask the iPhone to forget the pairing and you will also need to disable the widget in Settings and restart the Bangle by turning it off in Settings and then pressing BTN1 to restart. If you are loading apps from a different device, you simply need to turn off the iPhone bluetooth which will retain the pairing. You still need to disable the widget and restart the Bangle.
|
||||
|
||||

|
||||
|
||||
## Messages & Calls
|
||||
Messages are displayed as shown above until BTN2 is pressed to dismiss it. I strongly advise disabling the BTN2 LCD wake function in the Settings App as otherwise when the screen times out and you press BTN2 to wake the LCD, the screen will turn on and the Message Alert will be dismissed!. Calls can be answered or dropped.
|
||||
|
||||
 
|
||||
|
||||
|
||||
## Issues
|
||||
1. With GadgetBridge, the Android phone has a Central-Client role with the Bangle as Peripheral-Server. With the ANCS widget there is the fairly unusual situation in which the Bangle is Peripheral-Client to the iPhone's Central-Server role. Since Espruino does not deal explicitly with Bangle as Peripheral-Client an additional function has been added in the modified firmware: `var gatt = NRF.getGattforCentralServer(addr);`. This returns a bluetooth remote GATT server given the address of the iPhone which has just connected to the Bangle. With the standard firmware, the widget reconnects to the iPhone as a Client - however this has greatly degraded performance. See [Issue 1800.](https://github.com/espruino/Espruino/issues/1800) for more details.
|
||||
|
||||
2. When the Bangle switches apps, all state - including widget state - is lost unless explicitly stored. The consequence of this is that when the Bangle switches apps, the connection to iPhone has to be re-established to restore the remote GATT server and characteristics state. This is quite slow. To minimise reconnection, the widget needs to grab the screen from the running app to signal messages and calls. To allow this to work, the app needs to implement the `SCREENACCESS` interface. In essence, the widget only connects when running with compatible apps that implement this interface. An example implementation is:
|
||||
|
||||
```
|
||||
var SCREENACCESS = {
|
||||
withApp:true,
|
||||
request:function(){
|
||||
this.withApp=false;
|
||||
stopdraw(); //clears redraw timers etc
|
||||
clearWatch(); //clears button handlers
|
||||
},
|
||||
release:function(){
|
||||
this.withApp=true;
|
||||
startdraw(); //redraw app screen, restart timers etc
|
||||
setButtons(); //install button event handlers
|
||||
}
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower',function(on) {
|
||||
if (!SCREENACCESS.withApp) return;
|
||||
if (on) {
|
||||
startdraw();
|
||||
} else {
|
||||
stopdraw();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Please report bugs etc. by raising an issue [here](https://github.com/jeffmer/JeffsBangleAppsDev).
|
|
@ -1,266 +0,0 @@
|
|||
(() => {
|
||||
|
||||
var s = require("Storage").readJSON("widancs.json",1)||{settings:{enabled:false, category:[1,2,4]}};
|
||||
var ENABLED = s.settings.enabled;
|
||||
var CATEGORY = s.settings.category;
|
||||
|
||||
function advert(){
|
||||
NRF.setAdvertising([
|
||||
0x02, //length
|
||||
0x01, //flags
|
||||
0x06, //
|
||||
0x11, //length
|
||||
0x15, //solicited Service UUID
|
||||
0xD0,0x00,0x2D,0x12,0x1E,0x4B,
|
||||
0x0F,0xA4,
|
||||
0x99,0x4E,
|
||||
0xCE,0xB5,
|
||||
0x31,0xF4,0x05,0x79],{connectable:true,discoverable:true,interval:375});
|
||||
}
|
||||
|
||||
var state = {
|
||||
gatt:null,
|
||||
ancs:null,
|
||||
current:{cat:0,uid:0},
|
||||
notqueue:[],
|
||||
msgTO:undefined,
|
||||
com:new Uint8Array([0,0,0,0,0,1,20,0,3,100,0]),
|
||||
buf:new Uint8Array(132),
|
||||
inp:0,
|
||||
store:function(b){
|
||||
var i = this.inp;
|
||||
if (i+b.length<=132){
|
||||
this.buf.set(b,i);
|
||||
this.inp+=b.length;
|
||||
}
|
||||
},
|
||||
gotmsg:function(){
|
||||
var n = this.inp;
|
||||
var vw = DataView(this.buf.buffer);
|
||||
if (n<8) return null;
|
||||
var tn=vw.getUint16(6,true);
|
||||
if (n<(tn+8)) return null;
|
||||
var mn=vw.getUint16(9+tn,true);
|
||||
if (n<(mn+tn+11)) return null;
|
||||
return {tlen:tn, mlen:mn};
|
||||
}
|
||||
};
|
||||
|
||||
//stop advertising when peripheral link disconnected
|
||||
if (!NRF.getGattforCentralServer && ENABLED && typeof SCREENACCESS!='undefined')
|
||||
NRF.on('disconnect',function(reason){
|
||||
NRF.sleep();
|
||||
});
|
||||
|
||||
if (ENABLED && typeof SCREENACCESS!='undefined')
|
||||
NRF.on('connect',function(addr){
|
||||
if(NRF.getGattforCentralServer)
|
||||
do_bond(NRF.getGattforCentralServer(addr));
|
||||
else
|
||||
NRF.connect(addr).then(do_bond);
|
||||
});
|
||||
|
||||
function do_bond(g) {
|
||||
var tval, ival;
|
||||
state.gatt = g;
|
||||
function cleanup(){
|
||||
drawIcon(0); //disconnect from iPhone
|
||||
delete state.gatt;
|
||||
delete state.ancs;
|
||||
if(!NRF.getGattforCentralServer) NRF.disconnect();
|
||||
setTimeout(()=>{NRF.wake();},500);
|
||||
}
|
||||
drawIcon(1); //connect from iPhone
|
||||
state.gatt.device.on('gattserverdisconnected', function(reason) {
|
||||
if (ival) clearInterval(ival);
|
||||
if (tval) clearInterval(tval);
|
||||
cleanup();
|
||||
});
|
||||
E.on("kill",function(){
|
||||
state.gatt.disconnect().then(function(){NRF.sleep();});
|
||||
});
|
||||
NRF.setSecurity({passkey:"123456",mitm:1,display:1});
|
||||
tval = setTimeout(function(){
|
||||
if (ival) clearInterval(ival);
|
||||
state.gatt.disconnect().then(cleanup);
|
||||
},10000);
|
||||
state.gatt.startBonding().then(function(){
|
||||
ival = setInterval(function(){
|
||||
var sec = state.gatt.getSecurityStatus();
|
||||
if (!sec.connected) {clearInterval(ival); clearTimeout(tval); return;}
|
||||
if (sec.connected && sec.encrypted){
|
||||
clearInterval(ival);
|
||||
clearTimeout(tval);
|
||||
drawIcon(2); //bonded to iPhone
|
||||
do_ancs();
|
||||
return;
|
||||
}
|
||||
},1000);
|
||||
}).catch(function(e){
|
||||
Terminal.println("ERROR "+e);
|
||||
});
|
||||
}
|
||||
|
||||
function do_ancs() {
|
||||
state.ancs = {primary:null, notify:null, control:null, data:null};
|
||||
state.gatt.getPrimaryService("7905F431-B5CE-4E99-A40F-4B1E122D00D0").then(function(s) {
|
||||
state.ancs.primary=s;
|
||||
return s.getCharacteristic("9FBF120D-6301-42D9-8C58-25E699A21DBD");
|
||||
}).then(function(c) {
|
||||
state.ancs.notify=c;
|
||||
return state.ancs.primary.getCharacteristic("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9");
|
||||
}).then(function(c) {
|
||||
state.ancs.control=c;
|
||||
return state.ancs.primary.getCharacteristic("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB");
|
||||
}).then(function(c) {
|
||||
state.ancs.data =c;
|
||||
drawIcon(3);//got remote services
|
||||
state.ancs.notify.on('characteristicvaluechanged', function(ev) {
|
||||
getnotify(ev.target.value);
|
||||
});
|
||||
state.ancs.data.on('characteristicvaluechanged', function(e) {
|
||||
state.store(e.target.value.buffer);
|
||||
var inds = state.gotmsg();
|
||||
if (inds) printmsg(state.buf,inds);
|
||||
});
|
||||
state.ancs.notify.startNotifications().then(function(){
|
||||
state.ancs.data.startNotifications().then(function(){
|
||||
drawIcon(4); //ready for messages
|
||||
});
|
||||
});
|
||||
}).catch(function(e){
|
||||
Terminal.println("ERROR "+e);
|
||||
});
|
||||
}
|
||||
|
||||
function wordwrap(s){
|
||||
var txt = s.split("\n");
|
||||
var MAXCHARS = 18;
|
||||
for (var i = 0; i < txt.length; i++) {
|
||||
txt[i] = txt[i].trim();
|
||||
var l = txt[i];
|
||||
if (l.length > MAXCHARS) {
|
||||
var p = MAXCHARS;
|
||||
while (p > MAXCHARS - 8 && !" \t-_".includes(l[p]))
|
||||
p--;
|
||||
if (p == MAXCHARS - 8) p = MAXCHARS;
|
||||
txt[i] = l.substr(0, p);
|
||||
txt.splice(i + 1, 0, l.substr(p));
|
||||
}
|
||||
}
|
||||
return txt.join("\n");
|
||||
}
|
||||
|
||||
|
||||
var buzzing =false;
|
||||
var screentimeout = undefined;
|
||||
var inalert = false;
|
||||
|
||||
function release_screen(){
|
||||
screentimeout= setTimeout(() => {
|
||||
SCREENACCESS.release();
|
||||
screentimeout = undefined;
|
||||
inalert=false;
|
||||
next_notify();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function printmsg(buf,inds){
|
||||
|
||||
function send_action(tf){
|
||||
var bb = new Uint8Array(6);
|
||||
var v = DataView(bb.buffer);
|
||||
v.setUint8(0,2);
|
||||
v.setUint32(1,state.current.uid,true);
|
||||
v.setUint8(5,tf?0:1 );
|
||||
state.ancs.control.writeValue(bb).then(release_screen);
|
||||
}
|
||||
|
||||
if (state.msgTO) clearTimeout(state.msgTO);
|
||||
var title="";
|
||||
for (var i=8;i<8+inds.tlen; ++i) title+=String.fromCharCode(buf[i]);
|
||||
var message = "";
|
||||
for (var j=11+inds.tlen;j<11+inds.tlen+inds.mlen;++j) {
|
||||
message+=String.fromCharCode(buf[j]);
|
||||
}
|
||||
message = wordwrap(message);
|
||||
//we may already be displaying a prompt, so clear it
|
||||
E.showPrompt();
|
||||
if (screentimeout) clearTimeout(screentimeout);
|
||||
if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
|
||||
Bangle.setLCDPower(true);
|
||||
}
|
||||
SCREENACCESS.request();
|
||||
if (!buzzing && !(require('Storage').readJSON('setting.json',1)||{}).quiet){
|
||||
buzzing=true;
|
||||
Bangle.buzz(500).then(()=>{buzzing=false;});
|
||||
}
|
||||
if (state.current.cat!=1){
|
||||
E.showAlert(message,title).then(send_action.bind(null,false));
|
||||
} else {
|
||||
E.showPrompt(message,{title:title,buttons:{"Accept":true,"Cancel":false}}).then(send_action);
|
||||
}
|
||||
}
|
||||
|
||||
var notifyTO;
|
||||
function getnotify(d){
|
||||
var eid = d.getUint8(0);
|
||||
var ct = d.getUint8(2);
|
||||
var id = d.getUint32(4,true);
|
||||
if (eid>1) return;
|
||||
if (notifyTO) clearTimeout(notifyTO);
|
||||
if(!CATEGORY.includes(ct)) return;
|
||||
var len = state.notqueue.length;
|
||||
if (ct == 1) { // it's a call so pre-empt
|
||||
if (inalert) {state.notqueue.push(state.current); inalert=false;}
|
||||
state.notqueue.push({cat:ct, uid:id});
|
||||
} else if (len<16)
|
||||
state.notqueue[len] = {cat:ct, uid:id};
|
||||
notifyTO = setTimeout(next_notify,1000);
|
||||
}
|
||||
|
||||
function next_notify(){
|
||||
if(state.notqueue.length==0 || inalert) return;
|
||||
inalert=true;
|
||||
state.current = state.notqueue.pop();
|
||||
var v = DataView(state.com.buffer);
|
||||
if (state.current.cat==6) v.setUint8(8,2); else v.setUint8(8,3);//get email title
|
||||
v.setUint32(1,state.current.uid,true);
|
||||
state.inp=0;
|
||||
state.ancs.control.writeValue(state.com).then(function(){
|
||||
state.msgTO=setTimeout(()=>{
|
||||
inalert=false;
|
||||
state.msgTO=undefined;
|
||||
next_notify();
|
||||
},1000);
|
||||
});
|
||||
}
|
||||
|
||||
var stage = 5;
|
||||
//grey, pink, lightblue, yellow, green
|
||||
function draw(){
|
||||
var colors = new Uint16Array([0xc618,0xf818,0x3ff,0xffe0,0x07e0,0x0000]);
|
||||
var img = E.toArrayBuffer(atob("GBgBAAAABAAADgAAHwAAPwAAf4AAP4AAP4AAP4AAHwAAH4AAD8AAB+AAA/AAAfgAAf3gAH/4AD/8AB/+AA/8AAf4AAHwAAAgAAAA"));
|
||||
g.setColor(colors[stage]);
|
||||
g.drawImage(img,this.x,this.y);
|
||||
}
|
||||
|
||||
WIDGETS["ancs"] ={area:"tl", width:24,draw:draw};
|
||||
|
||||
function drawIcon(id){
|
||||
stage = id;
|
||||
WIDGETS["ancs"].draw();
|
||||
}
|
||||
|
||||
if (ENABLED && typeof SCREENACCESS!='undefined') {
|
||||
stage = 0;
|
||||
NRF.setServices(undefined,{uart:false});
|
||||
NRF.sleep();
|
||||
NRF.wake();
|
||||
advert();
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
(function(){function t(a){function e(){k(0);delete b.gatt;delete b.ancs;NRF.getGattforCentralServer||NRF.disconnect();setTimeout(function(){NRF.wake()},500)}var d;b.gatt=a;k(1);b.gatt.device.on("gattserverdisconnected",function(a){d&&clearInterval(d);c&&clearInterval(c);e()});E.on("kill",function(){b.gatt.disconnect().then(function(){NRF.sleep()})});NRF.setSecurity({passkey:"123456",mitm:1,display:1});var c=setTimeout(function(){d&&clearInterval(d);b.gatt.disconnect().then(e)},1E4);b.gatt.startBonding().then(function(){d=
|
||||
setInterval(function(){var a=b.gatt.getSecurityStatus();a.connected?a.connected&&a.encrypted&&(clearInterval(d),clearTimeout(c),k(2),v()):(clearInterval(d),clearTimeout(c))},1E3)})["catch"](function(a){Terminal.println("ERROR "+a)})}function v(){b.ancs={primary:null,notify:null,control:null,data:null};b.gatt.getPrimaryService("7905F431-B5CE-4E99-A40F-4B1E122D00D0").then(function(a){b.ancs.primary=a;return a.getCharacteristic("9FBF120D-6301-42D9-8C58-25E699A21DBD")}).then(function(a){b.ancs.notify=
|
||||
a;return b.ancs.primary.getCharacteristic("69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9")}).then(function(a){b.ancs.control=a;return b.ancs.primary.getCharacteristic("22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB")}).then(function(a){b.ancs.data=a;k(3);b.ancs.notify.on("characteristicvaluechanged",function(a){var e=a.target.value,c=e.getUint8(0);a=e.getUint8(2);e=e.getUint32(4,!0);1<c||(m&&clearTimeout(m),w.includes(a)&&(c=b.notqueue.length,1==a?(f&&(b.notqueue.push(b.current),f=!1),b.notqueue.push({cat:a,uid:e})):
|
||||
16>c&&(b.notqueue[c]={cat:a,uid:e}),m=setTimeout(n,1E3)))});b.ancs.data.on("characteristicvaluechanged",function(a){b.store(a.target.value.buffer);(a=b.gotmsg())&&x(b.buf,a)});b.ancs.notify.startNotifications().then(function(){b.ancs.data.startNotifications().then(function(){k(4)})})})["catch"](function(a){Terminal.println("ERROR "+a)})}function y(a){a=a.split("\n");for(var b=0;b<a.length;b++){a[b]=a[b].trim();var d=a[b];if(18<d.length){for(var c=18;10<c&&!" \t-_".includes(d[c]);)c--;10==c&&(c=18);
|
||||
a[b]=d.substr(0,c);a.splice(b+1,0,d.substr(c))}}return a.join("\n")}function z(){l=setTimeout(function(){SCREENACCESS.release();l=void 0;f=!1;n()},500)}function x(a,e){function d(a){var e=new Uint8Array(6),c=DataView(e.buffer);c.setUint8(0,2);c.setUint32(1,b.current.uid,!0);c.setUint8(5,a?0:1);b.ancs.control.writeValue(e).then(z)}b.msgTO&&clearTimeout(b.msgTO);for(var c="",h=8;h<8+e.tlen;++h)c+=String.fromCharCode(a[h]);h="";for(var f=11+e.tlen;f<11+e.tlen+e.mlen;++f)h+=String.fromCharCode(a[f]);
|
||||
h=y(h);E.showPrompt();l&&clearTimeout(l);Bangle.setLCDPower(!0);SCREENACCESS.request();p||(p=!0,Bangle.buzz(500).then(function(){p=!1}));1!=b.current.cat?E.showAlert(h,c).then(d.bind(null,!1)):E.showPrompt(h,{title:c,buttons:{Accept:!0,Cancel:!1}}).then(d)}function n(){if(0!=b.notqueue.length&&!f){f=!0;b.current=b.notqueue.pop();var a=DataView(b.com.buffer);6==b.current.cat?a.setUint8(8,2):a.setUint8(8,3);a.setUint32(1,b.current.uid,!0);b.inp=0;b.ancs.control.writeValue(b.com).then(function(){b.msgTO=
|
||||
setTimeout(function(){f=!1;b.msgTO=void 0;n()},1E3)})}}function k(a){q=a;WIDGETS.ancs.draw()}var u=require("Storage").readJSON("widancs.json",1)||{settings:{enabled:!1,category:[1,2,4]}},r=u.settings.enabled,w=u.settings.category,b={gatt:null,ancs:null,current:{cat:0,uid:0},notqueue:[],msgTO:void 0,com:new Uint8Array([0,0,0,0,0,1,20,0,3,100,0]),buf:new Uint8Array(132),inp:0,store:function(a){var b=this.inp;132>=b+a.length&&(this.buf.set(a,b),this.inp+=a.length)},gotmsg:function(){var a=this.inp,b=
|
||||
DataView(this.buf.buffer);if(8>a)return null;var d=b.getUint16(6,!0);if(a<d+8)return null;b=b.getUint16(9+d,!0);return a<b+d+11?null:{tlen:d,mlen:b}}};if(!NRF.getGattforCentralServer&&r&&"undefined"!=typeof SCREENACCESS)NRF.on("disconnect",function(a){NRF.sleep()});if(r&&"undefined"!=typeof SCREENACCESS)NRF.on("connect",function(a){NRF.getGattforCentralServer?t(NRF.getGattforCentralServer(a)):NRF.connect(a).then(t)});var p=!1,l=void 0,f=!1,m,q=5;WIDGETS.ancs={area:"tl",width:24,draw:function(){var a=
|
||||
new Uint16Array([50712,63512,1023,65504,2016,0]),b=E.toArrayBuffer(atob("GBgBAAAABAAADgAAHwAAPwAAf4AAP4AAP4AAP4AAHwAAH4AAD8AAB+AAA/AAAfgAAf3gAH/4AD/8AB/+AA/8AAf4AAHwAAAgAAAA"));g.setColor(a[q]);g.drawImage(b,this.x,this.y)}};r&&"undefined"!=typeof SCREENACCESS&&(q=0,NRF.setServices(void 0,{uart:!1}),NRF.sleep(),NRF.wake(),NRF.setAdvertising([2,1,6,17,21,208,0,45,18,30,75,15,164,153,78,206,181,49,244,5,121],{connectable:!0,discoverable:!0,interval:375}))})();
|
||||
|
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 48 KiB |
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"id": "widancs",
|
||||
"name": "Apple Notification Widget",
|
||||
"shortName": "ANCS Widget",
|
||||
"version": "0.07",
|
||||
"description": "Displays call, message etc notifications from a paired iPhone. Read README before installation as it only works with compatible apps",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
"tags": "widget",
|
||||
"supports": ["BANGLEJS"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"widancs.wid.js","url":"ancs.min.js"},
|
||||
{"name":"widancs.settings.js","url":"settings.js"}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 36 KiB |
|
@ -1,61 +0,0 @@
|
|||
(function(back) {
|
||||
const ANCSFILE = "widancs.json";
|
||||
|
||||
// initialize with default settings...
|
||||
let s = {
|
||||
'enabled': false,
|
||||
'category':[1,2,4]
|
||||
};
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage');
|
||||
const d = storage.readJSON(ANCSFILE, 1) || {};
|
||||
const saved = d.settings || {};
|
||||
for (const key in saved) {
|
||||
s[key] = saved[key];
|
||||
}
|
||||
|
||||
function save() {
|
||||
d.settings = s;
|
||||
storage.write(ANCSFILE, d);
|
||||
}
|
||||
|
||||
function setcategory(){
|
||||
const names = ["Other","Call ","Missed Call","Voicemail","Messages ","Calendar","Email","News ","Fitness ","Busniness","Location ","Entertainment"];
|
||||
function hascat(n){return s.category.includes(n);}
|
||||
function setcat(n,v){
|
||||
if (v)
|
||||
s.category.push(n);
|
||||
else
|
||||
s.category = s.category.filter((v,i,a)=>{return v!=n;});
|
||||
}
|
||||
const menu = {
|
||||
'': { 'title': 'Set Categories' }
|
||||
};
|
||||
for (var i=0; i<names.length;++i)
|
||||
menu[names[i]]={
|
||||
value:hascat(i),
|
||||
format:v=>v?'Yes':'No',
|
||||
onchange:setcat.bind(null,i)
|
||||
};
|
||||
menu['< Back'] = ()=>{save(); showMain();};
|
||||
return E.showMenu(menu);
|
||||
}
|
||||
|
||||
function showMain(){
|
||||
return E.showMenu({
|
||||
'Enable ANCS': {
|
||||
value: s.enabled,
|
||||
format: () => (s.enabled ? 'Yes' : 'No'),
|
||||
onchange: () => {
|
||||
s.enabled = !s.enabled;
|
||||
save();
|
||||
},
|
||||
},
|
||||
'Set Category':setcategory,
|
||||
'< Back': back,
|
||||
});
|
||||
}
|
||||
|
||||
showMain();
|
||||
});
|
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 50 KiB |
|
@ -1 +1,2 @@
|
|||
0.01: Created
|
||||
0.02: Set sort order to -10 so always display in right hand corner
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
"shortName":"Battery Theme",
|
||||
"icon": "widbata.png",
|
||||
"screenshots": [{"url":"screenshot_widbata_1.png"}],
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"type": "widget",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"description": "Shows the current battery level status in the top right using the clocks colour theme",
|
||||
"tags": "widget,battery",
|
||||
"sortorder": -10,
|
||||
"storage": [
|
||||
{"name":"widbata.wid.js","url":"widbata.wid.js"}
|
||||
]
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Handle new firmwares with 'lock' event
|
||||
0.03: Don't try to be fancy - just bail out on firmwares without a lock event
|
||||
0.04: Set sortorder to -1 so that widget always takes up the furthest left position
|
||||
0.05: Set sortorder to -10 so that others can take -1 etc
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"id": "widlock",
|
||||
"name": "Lock Widget",
|
||||
"version": "0.04",
|
||||
"version": "0.05",
|
||||
"description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked",
|
||||
"icon": "widget.png",
|
||||
"type": "widget",
|
||||
"tags": "widget,lock",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"sortorder": -1,
|
||||
"sortorder": -10,
|
||||
"storage": [
|
||||
{"name":"widlock.wid.js","url":"widget.js"}
|
||||
]
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: First release
|
||||
0.02: Size widget after step count is reset
|
||||
0.03: set sortorder to -1
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
"shortName":"Simple Pedometer",
|
||||
"icon": "screenshot_widpa.png",
|
||||
"screenshots": [{"url":"screenshot_widpa.png"}],
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"type": "widget",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in 12x16 font, requires firmware v2.11.21 or later",
|
||||
"tags": "widget,battery",
|
||||
"sortorder": -1,
|
||||
"storage": [
|
||||
{"name":"widpa.wid.js","url":"widpa.wid.js"}
|
||||
]
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
0.01: First release
|
||||
0.02: Fixed widget id to wibpb, Size widget after step count is reset
|
||||
0.03: Fixed widget id in onStep() come on get it right!
|
||||
0.04: set sortorder to -1
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "widpb",
|
||||
"name": "Lato Pedometer",
|
||||
"shortName":"Lato Pedometer",
|
||||
"icon": "screenshot_widpb.png",
|
||||
"screenshots": [{"url":"screenshot_widpb.png"}],
|
||||
"version":"0.04",
|
||||
"type": "widget",
|
||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"description": "Displays the current step count from `Bangle.getHealthStatus(\"day\").steps` in the Lato font, requires firmware v2.11.21 or later",
|
||||
"tags": "widget,battery",
|
||||
"sortorder": -1,
|
||||
"storage": [
|
||||
{"name":"widpb.wid.js","url":"widpb.wid.js"}
|
||||
]
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
// on.step version
|
||||
Bangle.on('step', function(s) { WIDGETS["bata"].draw(); });
|
||||
Bangle.on('step', function(s) { WIDGETS["widpb"].draw(); });
|
||||
Bangle.on('lcdPower', function(on) {
|
||||
if (on) WIDGETS["widpb"].draw();
|
||||
});
|
||||
|
|