Merge branch 'master' into development

master
xxDUxx 2023-01-17 14:35:26 +01:00 committed by GitHub
commit 58a3ee8a04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
570 changed files with 10293 additions and 4687 deletions

View File

@ -4,4 +4,5 @@ apps/schoolCalendar/fullcalendar/main.js
apps/authentiwatch/qr_packed.js apps/authentiwatch/qr_packed.js
apps/qrcode/qr-scanner.umd.min.js apps/qrcode/qr-scanner.umd.min.js
apps/gipy/pkg/gpconv.js apps/gipy/pkg/gpconv.js
apps/health/chart.min.js
*.test.js *.test.js

View File

@ -58,3 +58,7 @@ body:
validations: validations:
required: true required: true
- type: textarea
id: apps
attributes:
label: Installed apps

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ _site
.owncloudsync.log .owncloudsync.log
Desktop.ini Desktop.ini
.sync_*.db* .sync_*.db*
*.swp

View File

@ -98,7 +98,7 @@ This is the best way to test...
**Note:** It's a great idea to get a local copy of the repository on your PC, **Note:** It's a great idea to get a local copy of the repository on your PC,
then run `bin/sanitycheck.js` - it'll run through a bunch of common issues then run `bin/sanitycheck.js` - it'll run through a bunch of common issues
that there might be. that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule --init && git submodule update`.
Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect. Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect.
@ -282,8 +282,11 @@ and which gives information about the app for the Launcher.
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above) "dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID "dependencies" : { "messages":"app" } // optional, depend on a specific app ID
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify' // for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
"dependencies" : { "messageicons":"module" } // optional, depend on a specific library to be used with 'require' "dependencies" : { "messageicons":"module" } // optional, depend on a specific library to be used with 'require' - see provides_modules
"dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets
"provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require' "provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require'
"provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message'
"default" : true, // set if an app is the default implementer of something (a widget/module/etc)
"readme": "README.md", // if supplied, a link to a markdown-style text file "readme": "README.md", // if supplied, a link to a markdown-style text file
// that contains more information about this app (usage, etc) // that contains more information about this app (usage, etc)
// A 'Read more...' link will be added under the app // A 'Read more...' link will be added under the app

View File

@ -1,140 +1,142 @@
class TwoK { { // wrap app in scope to prevent minifier from removing the class definition completely
constructor() { class TwoK {
this.b = Array(4).fill().map(() => Array(4).fill(0)); constructor() {
this.score = 0; this.b = Array(4).fill().map(() => Array(4).fill(0));
this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"}; this.score = 0;
} this.cmap = {0: "#caa", 2:"#ccc", 4: "#bcc", 8: "#ba6", 16: "#e61", 32: "#d20", 64: "#d00", 128: "#da0", 256: "#ec0", 512: "#dd0"};
drawBRect(x1, y1, x2, y2, th, c, cf, fill) { }
g.setColor(c); drawBRect(x1, y1, x2, y2, th, c, cf, fill) {
for (i=0; i<th; ++i) g.drawRect(x1+i, y1+i, x2-i, y2-i); g.setColor(c);
if (fill) g.setColor(cf).fillRect(x1+th, y1+th, x2-th, y2-th); for (i=0; i<th; ++i) g.drawRect(x1+i, y1+i, x2-i, y2-i);
} if (fill) g.setColor(cf).fillRect(x1+th, y1+th, x2-th, y2-th);
render() { }
const yo = 20; render() {
const xo = yo/2; const yo = 20;
h = g.getHeight()-yo; const xo = yo/2;
w = g.getWidth()-yo; h = g.getHeight()-yo;
bh = Math.floor(h/4); w = g.getWidth()-yo;
bw = Math.floor(w/4); bh = Math.floor(h/4);
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0); bw = Math.floor(w/4);
g.setFont("Vector", 16).setColor(g.theme.fg).drawString("Score:"+this.score.toString(), g.getWidth()/2, 8); g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false); g.setFont("Vector", 16).setColor(g.theme.fg).drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
for (y=0; y<4; ++y) this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
for (x=0; x<4; ++x) { for (y=0; y<4; ++y)
b = this.b[y][x]; for (x=0; x<4; ++x) {
this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true); b = this.b[y][x];
if (b > 4) g.setColor(1, 1, 1); this.drawBRect(xo+x*bw, yo+y*bh-1, xo+(x+1)*bh-1, yo+(y+1)*bh-2, 4, "#a88", this.cmap[b], true);
else g.setColor(0, 0, 0); if (b > 4) g.setColor(1, 1, 1);
g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7)); else g.setColor(0, 0, 0);
if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh); g.setFont("Vector", bh*(b>8 ? (b>64 ? (b>512 ? 0.32 : 0.4) : 0.6) : 0.7));
} if (b>0) g.drawString(b.toString(), xo+(x+0.5)*bw+1, yo+(y+0.5)*bh);
} }
shift(d) { // +/-1: shift x, +/- 2: shift y }
var crc = E.CRC32(this.b.toString()); shift(d) { // +/-1: shift x, +/- 2: shift y
if (d==-1) { // shift x left var crc = E.CRC32(this.b.toString());
for (y=0; y<4; ++y) { if (d==-1) { // shift x left
for (x=2; x>=0; x--) for (y=0; y<4; ++y) {
if (this.b[y][x]==0) { for (x=2; x>=0; x--)
for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1]; if (this.b[y][x]==0) {
this.b[y][3] = 0; for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
}
for (x=0; x<3; ++x)
if (this.b[y][x]==this.b[y][x+1]) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y][x+1];
for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1];
this.b[y][3] = 0; this.b[y][3] = 0;
} }
for (x=0; x<3; ++x)
if (this.b[y][x]==this.b[y][x+1]) {
this.score += 2*this.b[y][x];
this.b[y][x] += this.b[y][x+1];
for (j=x+1; j<3; ++j) this.b[y][j] = this.b[y][j+1];
this.b[y][3] = 0;
}
}
} }
} else if (d==1) { // shift x right
else if (d==1) { // shift x right for (y=0; y<4; ++y) {
for (y=0; y<4; ++y) { for (x=1; x<4; x++)
for (x=1; x<4; x++) if (this.b[y][x]==0) {
if (this.b[y][x]==0) { for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1]; this.b[y][0] = 0;
this.b[y][0] = 0; }
} for (x=3; x>0; --x)
for (x=3; x>0; --x) if (this.b[y][x]==this.b[y][x-1]) {
if (this.b[y][x]==this.b[y][x-1]) { this.score += 2*this.b[y][x];
this.score += 2*this.b[y][x]; this.b[y][x] += this.b[y][x-1] ;
this.b[y][x] += this.b[y][x-1] ; for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1];
for (j=x-1; j>0; j--) this.b[y][j] = this.b[y][j-1]; this.b[y][0] = 0;
this.b[y][0] = 0; }
} }
} }
} else if (d==-2) { // shift y down
else if (d==-2) { // shift y down for (x=0; x<4; ++x) {
for (x=0; x<4; ++x) { for (y=1; y<4; y++)
for (y=1; y<4; y++) if (this.b[y][x]==0) {
if (this.b[y][x]==0) { for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x]; this.b[0][x] = 0;
this.b[0][x] = 0; }
} for (y=3; y>0; y--)
for (y=3; y>0; y--) if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) { this.score += 2*this.b[y][x];
this.score += 2*this.b[y][x]; this.b[y][x] += this.b[y-1][x];
this.b[y][x] += this.b[y-1][x]; for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x];
for (j=y-1; j>0; j--) this.b[j][x] = this.b[j-1][x]; this.b[0][x] = 0;
this.b[0][x] = 0; }
} }
} }
} else if (d==2) { // shift y up
else if (d==2) { // shift y up for (x=0; x<4; ++x) {
for (x=0; x<4; ++x) { for (y=2; y>=0; y--)
for (y=2; y>=0; y--) if (this.b[y][x]==0) {
if (this.b[y][x]==0) { for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x]; this.b[3][x] = 0;
this.b[3][x] = 0; }
} for (y=0; y<3; ++y)
for (y=0; y<3; ++y) if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) { this.score += 2*this.b[y][x];
this.score += 2*this.b[y][x]; this.b[y][x] += this.b[y+1][x];
this.b[y][x] += this.b[y+1][x]; for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x];
for (j=y+1; j<3; ++j) this.b[j][x] = this.b[j+1][x]; this.b[3][x] = 0;
this.b[3][x] = 0; }
} }
} }
return (E.CRC32(this.b.toString())!=crc);
}
addDigit() {
var d = Math.random()>0.9 ? 4 : 2;
var id = Math.floor(Math.random()*16);
while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16);
this.b[Math.floor(id/4)][id%4] = d;
} }
return (E.CRC32(this.b.toString())!=crc);
} }
addDigit() {
var d = Math.random()>0.9 ? 4 : 2;
var id = Math.floor(Math.random()*16);
while (this.b[Math.floor(id/4)][id%4] > 0) id = Math.floor(Math.random()*16);
this.b[Math.floor(id/4)][id%4] = d;
}
}
function dragHandler(e) { function dragHandler(e) {
if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) { if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
var res = false; var res = false;
if (Math.abs(e.dx)>Math.abs(e.dy)) { if (Math.abs(e.dx)>Math.abs(e.dy)) {
if (e.dx>0) res = twok.shift(1); if (e.dx>0) res = twok.shift(1);
if (e.dx<0) res = twok.shift(-1); if (e.dx<0) res = twok.shift(-1);
}
else {
if (e.dy>0) res = twok.shift(-2);
if (e.dy<0) res = twok.shift(2);
}
if (res) twok.addDigit();
twok.render();
} }
else {
if (e.dy>0) res = twok.shift(-2);
if (e.dy<0) res = twok.shift(2);
}
if (res) twok.addDigit();
twok.render();
} }
}
function swipeHandler() { function swipeHandler() {
} }
function buttonHandler() { function buttonHandler() {
} }
var twok = new TwoK(); var twok = new TwoK();
twok.addDigit(); twok.addDigit(); twok.addDigit(); twok.addDigit();
twok.render(); twok.render();
if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler); if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
if (process.env.HWVERSION==1) { if (process.env.HWVERSION==1) {
Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); }); Bangle.on("swipe", (e) => { res = twok.shift(e); if (res) twok.addDigit(); twok.render(); });
setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true}); setWatch(() => { res = twok.shift(2); if (res) twok.addDigit(); twok.render(); }, BTN1, {repeat: true});
setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true}); setWatch(() => { res = twok.shift(-2); if (res) twok.addDigit(); twok.render(); }, BTN3, {repeat: true});
} }
}

View File

@ -1,2 +1,3 @@
0.01: New app! 0.01: New app!
0.02: Better support for watch themes 0.02: Better support for watch themes
0.03: Workaround minifier bug

View File

@ -2,7 +2,7 @@
"name": "2047pp", "name": "2047pp",
"shortName":"2047pp", "shortName":"2047pp",
"icon": "app.png", "icon": "app.png",
"version":"0.02", "version":"0.03",
"description": "Bangle version of a tile shifting game", "description": "Bangle version of a tile shifting game",
"supports" : ["BANGLEJS","BANGLEJS2"], "supports" : ["BANGLEJS","BANGLEJS2"],
"allow_emulator": true, "allow_emulator": true,

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Fullscreen settings. 0.02: Fullscreen settings.
0.03: Tell clock widgets to hide. 0.03: Tell clock widgets to hide.
0.04: Use widget_utils.

View File

@ -1,6 +1,7 @@
const SETTINGS_FILE = "90sclk.setting.json"; const SETTINGS_FILE = "90sclk.setting.json";
const locale = require('locale'); const locale = require('locale');
const storage = require('Storage'); const storage = require('Storage');
const widget_utils = require('widget_utils');
/* /*
@ -109,7 +110,7 @@ function draw() {
// Draw widgets if not fullscreen // Draw widgets if not fullscreen
if(settings.fullscreen){ if(settings.fullscreen){
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} widget_utils.hide();
} else { } else {
Bangle.drawWidgets(); Bangle.drawWidgets();
} }

View File

@ -1,7 +1,7 @@
{ {
"id": "90sclk", "id": "90sclk",
"name": "90s Clock", "name": "90s Clock",
"version": "0.03", "version": "0.04",
"description": "A 90s style watch-face", "description": "A 90s style watch-face",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",

View File

@ -1 +1,4 @@
0.01: Beta version for Bangle 2 (2021/11/28) 0.01: Beta version for Bangle 2 (2021/11/28)
0.02: Shows night time on the map (2022/12/28)
0.03: Add 1 minute timer with upper taps (2023/01/05)
1.00: Page to set up custom time zones (2023/01/06)

View File

@ -2,14 +2,17 @@
* Works with Bangle 2 * Works with Bangle 2
* Timer * Timer
* Right tap: start/increase by 10 minutes; Left tap: decrease by 5 minutes * Top Right tap: increase by 1 minute
* Top Left tap: decrease by 1 minute
* Bottom Right tap: increase by 10 minutes
* Bottom Left tap: decrease by 5 minutes
* Short buzz at T-30, T-20, T-10 ; Double buzz at T * Short buzz at T-30, T-20, T-10 ; Double buzz at T
* Other time zones * Other time zones
* Currently hardcoded to Paris and Tokyo (this will be customizable in a future version) * Showing Paris and Tokyo by default, but you can customize this using the dedicated configuration page on the app store
* World Map * World Map
* The yellow line shows the position of the sun * The map shows day and night on Earth and the position of the Sun (yellow line)
![](screenshot.png) ![](screenshot-1.png) ![](screenshot.png)
## Creator ## Creator
[@alainsaas](https://github.com/alainsaas) [@alainsaas](https://github.com/alainsaas)

View File

@ -18,19 +18,29 @@ var timervalue = 0;
var istimeron = false; var istimeron = false;
var timertick; var timertick;
Bangle.on('touch',t=>{ Bangle.on('touch',(touchside, touchdata)=>{
if (t == 1) { if (touchside == 1) {
Bangle.buzz(30); Bangle.buzz(30);
if (timervalue < 5*60) { timervalue = 1 ; } var changevalue = 0;
else { timervalue -= 5*60; } if(touchdata.y > 88) {
changevalue += 60*5;
} else {
changevalue += 60*1;
}
if (timervalue < changevalue) { timervalue = 1 ; }
else { timervalue -= changevalue; }
} }
else if (t == 2) { else if (touchside == 2) {
Bangle.buzz(30); Bangle.buzz(30);
if (!istimeron) { if (!istimeron) {
istimeron = true; istimeron = true;
timertick = setInterval(countDown, 1000); timertick = setInterval(countDown, 1000);
} }
timervalue += 60*10; if(touchdata.y > 88) {
timervalue += 60*10;
} else {
timervalue += 60*1;
}
} }
}); });
@ -73,12 +83,13 @@ function countDown() {
function showWelcomeMessage() { function showWelcomeMessage() {
g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6);
g.setFontAlign(0, 0).setFont("6x8"); g.setFontAlign(0, 0).setFont("6x8");
g.drawString("Touch right to", 44, 80); g.drawString("Tap right to", 44, 80);
g.drawString("start timer", 44, 88); g.drawString("start timer", 44, 88);
setTimeout(function(){ g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); }, 8000); setTimeout(function(){ g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); }, 8000);
} }
// time // time
var offsets = require("Storage").readJSON("a_clock_timer.settings.json") || [ ["PAR",1], ["TYO",9] ];
var drawTimeout; var drawTimeout;
function getGmt() { function getGmt() {
@ -102,20 +113,34 @@ function queueNextDraw() {
function draw() { function draw() {
g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT); g.reset().clearRect(0,24,g.getWidth(),g.getHeight()-IMAGEHEIGHT);
g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT); g.drawImage(getImg(),0,g.getHeight()-IMAGEHEIGHT);
var x_sun = 176 - (getGmt().getHours() / 24 * 176 + 4); var gmtHours = getGmt().getHours();
var x_sun = 176 - (gmtHours / 24 * 176 + 4);
g.setColor('#ff0').drawLine(x_sun, g.getHeight()-IMAGEHEIGHT, x_sun, g.getHeight()); g.setColor('#ff0').drawLine(x_sun, g.getHeight()-IMAGEHEIGHT, x_sun, g.getHeight());
g.reset(); g.reset();
var x_night_start = (176 - (((gmtHours-6)%24) / 24 * 176 + 4)) % 176;
var x_night_end = 176 - (((gmtHours+6)%24) / 24 * 176 + 4);
g.setColor('#000');
for (let x = x_night_start; x < (x_night_end < x_night_start ? 176 : x_night_end); x+=2) {
g.drawLine(x, g.getHeight()-IMAGEHEIGHT, x, g.getHeight());
}
if (x_night_end < x_night_start) {
for (let x = 0; x < x_night_end; x+=2) {
g.drawLine(x, g.getHeight()-IMAGEHEIGHT, x, g.getHeight());
}
}
var locale = require("locale"); var locale = require("locale");
var date = new Date(); var date = new Date();
g.setFontAlign(0,0); g.setFontAlign(0,0);
g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 46); g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 46);
g.setFont("6x8"); g.setFont("6x8");
g.drawString(locale.date(new Date(),1), 125, 68); g.drawString(locale.date(new Date(),1), 125, 68);
g.drawString("PAR "+locale.time(getTimeFromTimezone(1),1), 125, 80); g.drawString(offsets[0][0]+" "+locale.time(getTimeFromTimezone(offsets[0][1]),1), 125, 80);
g.drawString("TYO "+locale.time(getTimeFromTimezone(9),1), 125, 88); g.drawString(offsets[1][0]+" "+locale.time(getTimeFromTimezone(offsets[1][1]),1), 125, 88);
queueNextDraw(); queueNextDraw();
} }

View File

@ -0,0 +1,58 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<p>You can set the 2 additional timezones displayed by the clock.</p>
<table id="a_clock_timer-offsets">
<tr>
<th>Name</th>
<th>UTC Offset (Hours)</th>
</tr>
</table>
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
<script src="../../core/lib/customize.js"></script>
<script>
var offsets=[];
try{
var stored = localStorage.getItem('a_clock_timer-offset-list')
if(stored) offsets = JSON.parse(stored);
if (!offsets || offsets.length!=2) {
throw "Offsets invalid";
}
} catch(e){
offsets=[
["PAR",1],
["TYO",9],
];
}
console.log(offsets);
var tbl=document.getElementById("a_clock_timer-offsets");
for (var i=0; i<2; i++) {
var $offset = document.createElement('tr')
$offset.innerHTML = `
<td><input type="text" size="4" maxlength="3" id="name_${i}" value="${offsets[i][0]}"></td>
<td><input type="number" id="offset_${i}" value="${offsets[i][1]}"></td>`
tbl.append($offset);
}
document.getElementById("upload").addEventListener("click", function() {
var storage_offsets=[];
var app_offsets=[];
for (var i=0; i<2; i++) {
var name=document.getElementById("name_"+i).value;
var offset=document.getElementById("offset_"+i).value;
app_offsets.push([name,offset]);
storage_offsets.push([name,offset]);
}
console.log(storage_offsets);
console.log(app_offsets);
localStorage.setItem('a_clock_timer-offset-list',JSON.stringify(storage_offsets));
sendCustomizedApp({
storage:[
{name:"a_clock_timer.settings.json", content:JSON.stringify(app_offsets)},
]
});
});
</script>
</body>
</html>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,2 +1,3 @@
1.00: Release (2021/12/01) 1.00: Release (2021/12/01)
1.01: Grey font when timer is frozen (2021/12/04) 1.01: Grey font when timer is frozen (2021/12/04)
1.02: Force light theme, since the app is not designed for dark theme (2022/12/28)

View File

@ -166,6 +166,7 @@ function draw() {
g.drawRect(88+8,138-24, 176-10, 138+22); g.drawRect(88+8,138-24, 176-10, 138+22);
} }
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
require("FontHaxorNarrow7x17").add(Graphics); require("FontHaxorNarrow7x17").add(Graphics);
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();

View File

@ -2,7 +2,7 @@
"id":"a_speech_timer", "id":"a_speech_timer",
"name":"Speech Timer", "name":"Speech Timer",
"icon": "app.png", "icon": "app.png",
"version":"1.01", "version":"1.02",
"description": "A timer designed to help keeping your speeches and presentations to time.", "description": "A timer designed to help keeping your speeches and presentations to time.",
"tags": "tool,timer", "tags": "tool,timer",
"readme":"README.md", "readme":"README.md",

View File

@ -7,3 +7,7 @@
0.07: Clkinfo improvements. 0.07: Clkinfo improvements.
0.08: Fix error in clkinfo (didn't require Storage & locale) 0.08: Fix error in clkinfo (didn't require Storage & locale)
Fix clkinfo icon Fix clkinfo icon
0.09: Ensure Agenda supplies an image for clkinfo items
0.10: Update clock_info to avoid a redraw
0.11: Setting to use "Today" and "Yesterday" instead of dates
Added dynamic, short and range fields to clkinfo

View File

@ -1,7 +1,14 @@
(function() { (function() {
function getPassedSec(date) {
var now = new Date();
var passed = (now-date)/1000;
if(passed<0) return 0;
return passed;
}
var agendaItems = { var agendaItems = {
name: "Agenda", name: "Agenda",
img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="), img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="),
dynamic: true,
items: [] items: []
}; };
var locale = require("locale"); var locale = require("locale");
@ -15,12 +22,16 @@
var title = entry.title.slice(0,12); var title = entry.title.slice(0,12);
var date = new Date(entry.timestamp*1000); var date = new Date(entry.timestamp*1000);
var dateStr = locale.date(date).replace(/\d\d\d\d/,""); var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
var shortStr = ((date-now) > 86400000 || entry.allDay) ? dateStr : locale.time(date,1);
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
agendaItems.items.push({ agendaItems.items.push({
name: "Agenda "+i, name: "Agenda "+i,
get: () => ({ text: title + "\n" + dateStr, img: null}), hasRange: true,
show: function() { agendaItems.items[i].emit("redraw"); }, get: () => ({ text: title + "\n" + dateStr,
img: agendaItems.img, short: shortStr.trim(),
v: getPassedSec(date), min: 0, max: entry.durationInSeconds}),
show: function() {},
hide: function () {} hide: function () {}
}); });
}); });

View File

@ -33,16 +33,32 @@ CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
function getDate(timestamp) { function getDate(timestamp) {
return new Date(timestamp*1000); return new Date(timestamp*1000);
} }
function formatDay(date) {
if (!settings.useToday) {
return Locale.date(date);
}
const dateformatted = date.toISOString().split('T')[0]; // yyyy-mm-dd
const today = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd
if (dateformatted == today) {
return /*LANG*/"Today ";
} else {
const tomorrow = new Date(Date.now() + 86400 * 1000).toISOString().split('T')[0]; // yyyy-mm-dd
if (dateformatted == tomorrow) {
return /*LANG*/"Tomorrow ";
}
return Locale.date(date);
}
}
function formatDateLong(date, includeDay, allDay) { function formatDateLong(date, includeDay, allDay) {
let shortTime = Locale.time(date,1)+Locale.meridian(date); let shortTime = Locale.time(date,1)+Locale.meridian(date);
if(allDay) shortTime = ""; if(allDay) shortTime = "";
if(includeDay || allDay) if(includeDay || allDay) {
return Locale.date(date)+" "+shortTime; return formatDay(date)+" "+shortTime;
}
return shortTime; return shortTime;
} }
function formatDateShort(date, allDay) { function formatDateShort(date, allDay) {
return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay? return formatDay(date).replace(/\d\d\d\d/,"")+(allDay?"":Locale.time(date,1)+Locale.meridian(date));
"" : Locale.time(date,1)+Locale.meridian(date));
} }
var lines = []; var lines = [];

View File

@ -1,7 +1,7 @@
{ {
"id": "agenda", "id": "agenda",
"name": "Agenda", "name": "Agenda",
"version": "0.08", "version": "0.11",
"description": "Simple agenda", "description": "Simple agenda",
"icon": "agenda.png", "icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],

View File

@ -43,6 +43,13 @@
updateSettings(); updateSettings();
} }
}, },
/*LANG*/"Use 'Today',..." : {
value : !!settings.useToday,
onchange: v => {
settings.useToday = v;
updateSettings();
}
},
}; };
E.showMenu(mainmenu); E.showMenu(mainmenu);
}) })

View File

@ -3,3 +3,6 @@
0.03: Do not load AGPS data on boot 0.03: Do not load AGPS data on boot
Increase minimum interval to 6 hours Increase minimum interval to 6 hours
0.04: Write AGPS data chunks with delay to improve reliability 0.04: Write AGPS data chunks with delay to improve reliability
0.05: Show last success date
Do not start A-GPS update automatically
0.06: Switch off gps after updating

View File

@ -23,12 +23,26 @@ Bangle.drawWidgets();
let waiting = false; let waiting = false;
function start() { function start(restart) {
g.reset(); g.reset();
g.clear(); g.clear();
waiting = false; waiting = false;
display("Retry?", "touch to retry"); if (!restart) {
display("Start?", "touch to start");
}
else {
display("Retry?", "touch to retry");
}
Bangle.on("touch", () => { updateAgps(); }); Bangle.on("touch", () => { updateAgps(); });
const file = "agpsdata.json";
let data = require("Storage").readJSON(file, 1) || {};
if (data.lastUpdate) {
g.setFont("Vector", 11);
g.drawString("last success:", 5, g.getHeight() - 22);
g.drawString(new Date(data.lastUpdate).toISOString(), 5, g.getHeight() - 11);
}
} }
function updateAgps() { function updateAgps() {
@ -36,7 +50,7 @@ function updateAgps() {
g.clear(); g.clear();
if (!waiting) { if (!waiting) {
waiting = true; waiting = true;
display("Updating A-GPS...", "takes ~ 10 seconds"); display("Updating A-GPS...", "takes ~10 seconds");
require("agpsdata").pull(function() { require("agpsdata").pull(function() {
waiting = false; waiting = false;
display("A-GPS updated.", "touch to close"); display("A-GPS updated.", "touch to close");
@ -45,10 +59,10 @@ function updateAgps() {
function(error) { function(error) {
waiting = false; waiting = false;
E.showAlert(error, "Error") E.showAlert(error, "Error")
.then(() => { start(); }); .then(() => { start(true); });
}); });
} else { } else {
display("Waiting..."); display("Waiting...");
} }
} }
updateAgps(); start(false);

View File

@ -10,20 +10,24 @@ readSettings();
function setAGPS(b64) { function setAGPS(b64) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on
const gnsstype = settings.gnsstype || 1; // default GPS const gnsstype = settings.gnsstype || 1; // default GPS
initCommands += `Serial1.println("${CASIC_CHECKSUM("$PCAS04," + gnsstype)}")\n`; // set GNSS mode
// What about: // What about:
// NAV-TIMEUTC (0x01 0x10) // NAV-TIMEUTC (0x01 0x10)
// NAV-PV (0x01 0x03) // NAV-PV (0x01 0x03)
// or AGPS.zip uses AID-INI (0x0B 0x01) // or AGPS.zip uses AID-INI (0x0B 0x01)
Bangle.setGPSPower(1,"agpsdata"); // turn GPS on
eval(initCommands); Serial1.println(CASIC_CHECKSUM("$PCAS04," + gnsstype)); // set GNSS mode
try { try {
writeChunks(atob(b64), resolve); writeChunks(atob(b64), ()=>{
setTimeout(()=>{
Bangle.setGPSPower(0,"agpsdata");
resolve();
}, 1000);
});
} catch (e) { } catch (e) {
console.log("error:", e); console.log("error:", e);
Bangle.setGPSPower(0,"agpsdata");
reject(); reject();
} }
}); });
@ -36,9 +40,8 @@ function writeChunks(bin, resolve) {
setTimeout(function() { setTimeout(function() {
if (chunkI < bin.length) { if (chunkI < bin.length) {
var chunk = bin.substr(chunkI, chunkSize); var chunk = bin.substr(chunkI, chunkSize);
js = `Serial1.write(atob("${btoa(chunk)}"))\n`; Serial1.write(atob(btoa(chunk)));
eval(js);
chunkI += chunkSize; chunkI += chunkSize;
writeChunks(bin, resolve); writeChunks(bin, resolve);
} else { } else {

View File

@ -2,7 +2,7 @@
"name": "A-GPS Data Downloader App", "name": "A-GPS Data Downloader App",
"shortName":"A-GPS Data", "shortName":"A-GPS Data",
"icon": "agpsdata.png", "icon": "agpsdata.png",
"version":"0.04", "version":"0.06",
"description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.", "description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.",
"tags": "boot,tool,assisted,gps,agps,http", "tags": "boot,tool,assisted,gps,agps,http",
"allow_emulator":true, "allow_emulator":true,

View File

@ -2,3 +2,6 @@
0.02: Design improvements and fixes. 0.02: Design improvements and fixes.
0.03: Indicate battery level through line occurrence. 0.03: Indicate battery level through line occurrence.
0.04: Use widget_utils module. 0.04: Use widget_utils module.
0.05: Support for clkinfo.
0.06: ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc.
0.07: Use clock_info.addInteractive instead of a custom implementation

View File

@ -10,7 +10,8 @@ The original output of stable diffusion is shown here:
My implementation is shown below. Note that horizontal lines occur randomly, but the My implementation is shown below. Note that horizontal lines occur randomly, but the
probability is correlated with the battery level. So if your screen contains only probability is correlated with the battery level. So if your screen contains only
a few lines its time to charge your bangle again ;) a few lines its time to charge your bangle again ;) Also note that the upper text
implements the clkinfo module and can be configured via touch and swipe left/right and up/down.
![](impl.png) ![](impl.png)

View File

@ -1,6 +1,13 @@
/** /************************************************
* AI Clock * AI Clock
*/ */
const clock_info = require("clock_info");
/************************************************
* Assets
*/
require("Font7x11Numeric7Seg").add(Graphics); require("Font7x11Numeric7Seg").add(Graphics);
Graphics.prototype.setFontGochiHand = function(scale) { Graphics.prototype.setFontGochiHand = function(scale) {
// Actual height 27 (29 - 3) // Actual height 27 (29 - 3)
@ -13,33 +20,14 @@ Graphics.prototype.setFontGochiHand = function(scale) {
return this; return this;
} }
/*
* Set some important constants such as width, height and center
*/
var W = g.getWidth(),R=W/2;
var H = g.getHeight();
var cx = W/2;
var cy = H/2;
var drawTimeout;
/* function drawBackground(start, end) {
* Based on the great multi clock from https://github.com/jeffmer/BangleApps/
*/
Graphics.prototype.drawRotRect = function(w, r1, r2, angle) {
angle = angle % 360;
var w2=w/2, h=r2-r1, theta=angle*Math.PI/180;
return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0],
{x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta}));
};
function drawBackground() {
g.setFontAlign(0,0); g.setFontAlign(0,0);
g.setColor(g.theme.fg); g.setColor("#000");
var bat = E.getBattery() / 100.0; var bat = E.getBattery() / 100.0;
var y = 0; var y = start;
while(y < H){ while(y < end){
// Show less lines in case of small battery level. // Show less lines in case of small battery level.
if(Math.random() > bat){ if(Math.random() > bat){
y += 5; y += 5;
@ -55,6 +43,30 @@ function drawBackground() {
} }
/************************************************
* Set some important constants such as width, height and center
*/
var W = g.getWidth(),R=W/2;
var H = g.getHeight();
var cx = W/2;
var cy = H/2;
var drawTimeout;
var clkInfoY = 60;
/*
* Based on the great multi clock from https://github.com/jeffmer/BangleApps/
*/
Graphics.prototype.drawRotRect = function(w, r1, r2, angle) {
angle = angle % 360;
var w2=w/2, h=r2-r1, theta=angle*Math.PI/180;
return this.fillPoly(this.transformVertices([-w2,0,-w2,-h,w2,-h,w2,0],
{x:cx+r1*Math.sin(theta),y:cy-r1*Math.cos(theta),rotate:theta}));
};
function drawCircle(isLocked){ function drawCircle(isLocked){
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.fillCircle(cx, cy, 12); g.fillCircle(cx, cy, 12);
@ -64,19 +76,12 @@ function drawCircle(isLocked){
g.fillCircle(cx, cy, 6); g.fillCircle(cx, cy, 6);
} }
function toAngle(a){
if (a < 0){
return 360 + a;
}
if(a > 360) {
return 360 - a;
}
return a
}
function drawTime(){ function drawTime(){
// Draw digital time first
drawDigits();
// And now the analog time
var drawHourHand = g.drawRotRect.bind(g,8,12,R-38); var drawHourHand = g.drawRotRect.bind(g,8,12,R-38);
var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 ); var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 );
@ -90,13 +95,6 @@ function drawTime(){
h += date.getMinutes()/60.0; h += date.getMinutes()/60.0;
h = parseInt(h*360/12); h = parseInt(h*360/12);
// Draw minute and hour bg
g.setColor(g.theme.bg);
drawHourHand(toAngle(h-3));
drawHourHand(toAngle(h+3));
drawMinuteHand(toAngle(m-2));
drawMinuteHand(toAngle(m+3));
// Draw minute and hour fg // Draw minute and hour fg
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
drawHourHand(h); drawHourHand(h);
@ -104,28 +102,6 @@ function drawTime(){
} }
function drawDate(){
var date = new Date();
g.setFontAlign(0,0);
g.setFontGochiHand();
var text = ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2);
var w = g.stringWidth(text);
g.setColor(g.theme.bg);
g.fillRect(cx-w/2-4, 20, cx+w/2+4, 40+12);
g.setColor(g.theme.fg);
// Draw right line as designed by stable diffusion
g.drawLine(cx+w/2+5, 20, cx+w/2+5, 40+12);
g.drawLine(cx+w/2+6, 20, cx+w/2+6, 40+12);
g.drawLine(cx+w/2+7, 20, cx+w/2+7, 40+12);
// And finally the text
g.drawString(text, cx, 40);
}
function drawDigits(){ function drawDigits(){
var date = new Date(); var date = new Date();
@ -156,19 +132,22 @@ function drawDigits(){
} }
function draw(){ function draw(){
// Note that we force a redraw also of the clock info as
// we want to ensure (for design purpose) that the hands
// are above the clkinfo section.
clockInfoMenu.redraw();
}
function drawMainClock(){
// Queue draw in one minute // Queue draw in one minute
queueDraw(); queueDraw();
g.setColor("#fff");
g.reset().clearRect(0, clkInfoY, g.getWidth(), g.getHeight());
g.reset(); drawBackground(clkInfoY, H);
g.clearRect(0, 0, g.getWidth(), g.getHeight());
g.setColor(1,1,1);
drawBackground();
drawDate();
drawDigits();
drawTime(); drawTime();
drawCircle(Bangle.isLocked()); drawCircle(Bangle.isLocked());
} }
@ -179,7 +158,7 @@ function draw(){
*/ */
Bangle.on('lcdPower',on=>{ Bangle.on('lcdPower',on=>{
if (on) { if (on) {
draw(true); draw();
} else { // stop draw timer } else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined; drawTimeout = undefined;
@ -191,6 +170,12 @@ Bangle.on('lock', function(isLocked) {
}); });
E.on("kill", function(){
clockInfoMenu.remove();
delete clockInfoMenu;
});
/* /*
* Some helpers * Some helpers
*/ */
@ -203,6 +188,54 @@ function queueDraw() {
} }
/************************************************
* Clock Info
*/
let clockInfoItems = clock_info.load();
let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
x : 0,
y: 0,
w: W,
h: clkInfoY,
draw : (itm, info, options) => {
g.setFontAlign(0,0);
g.setFont("Vector", 20);
g.setColor("#fff");
g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h);
drawBackground(0, clkInfoY+2);
// Set text and font
var image = info.img;
var text = String(info.text);
var imgWidth = image == null ? 0 : 24;
var strWidth = g.stringWidth(text);
var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2);
var w = imgWidth + strWidth;
// Draw right line as designed by stable diffusion
g.setColor(options.focus ? "#0f0" : "#fff");
g.fillRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2)
g.setColor("#000");
g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2);
g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2);
g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2);
// Draw text and image
g.drawString(text, cx+imgWidth/2, 42);
g.drawString(text, cx+1+imgWidth/2, 41);
if(image != null) {
var scale = image.width ? imgWidth / image.width : 1;
g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale});
}
drawMainClock();
}
});
/* /*
* Lets start widgets, listen for btn etc. * Lets start widgets, listen for btn etc.
@ -216,8 +249,9 @@ Bangle.loadWidgets();
* area to the top bar doesn't get cleared. * area to the top bar doesn't get cleared.
*/ */
require('widget_utils').hide(); require('widget_utils').hide();
// Clear the screen once, at startup and draw clock // Clear the screen once, at startup and draw clock
g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); g.setTheme({bg:"#fff",fg:"#000",dark:false});
draw(); draw();
// After drawing the watch face, we can draw the widgets // After drawing the watch face, we can draw the widgets

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
apps/aiclock/impl_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
apps/aiclock/impl_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -3,7 +3,7 @@
"name": "AI Clock", "name": "AI Clock",
"shortName":"AI Clock", "shortName":"AI Clock",
"icon": "aiclock.png", "icon": "aiclock.png",
"version":"0.04", "version":"0.07",
"readme": "README.md", "readme": "README.md",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.", "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
@ -11,7 +11,9 @@
"tags": "clock", "tags": "clock",
"screenshots": [ "screenshots": [
{"url":"orig.png"}, {"url":"orig.png"},
{"url":"impl.png"} {"url":"impl.png"},
{"url":"impl_2.png"},
{"url":"impl_3.png"}
], ],
"storage": [ "storage": [
{"name":"aiclock.app.js","url":"aiclock.app.js"}, {"name":"aiclock.app.js","url":"aiclock.app.js"},

View File

@ -36,4 +36,5 @@
0.33: Allow hiding timers&alarms 0.33: Allow hiding timers&alarms
0.34: Add "Confirm" option to alarm/timer edit menus 0.34: Add "Confirm" option to alarm/timer edit menus
0.35: Add automatic translation of more strings 0.35: Add automatic translation of more strings
0.36: alarm widget moved out of app
0.37: add message input and dated Events

View File

@ -1,15 +1,18 @@
# Alarms & Timers # Alarms & Timers
This app allows you to add/modify any alarms and timers. This app allows you to add/modify any alarms, timers and events.
Optional: When a keyboard app is detected, you can add a message to display when any of these is triggered.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps. It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps.
## Menu overview ## Menu overview
- `New...` - `New...`
- `New Alarm` &rarr; Configure a new alarm - `New Alarm` &rarr; Configure a new alarm (triggered based on time and day of week)
- `Repeat` &rarr; Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely) - `Repeat` &rarr; Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely)
- `New Timer` &rarr; Configure a new timer - `New Timer` &rarr; Configure a new timer (triggered based on amount of time elapsed in hours/minutes/seconds)
- `New Event` &rarr; Configure a new event (triggered based on time and date)
- `Advanced` - `Advanced`
- `Scheduler settings` &rarr; Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details - `Scheduler settings` &rarr; Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details
- `Enable All` &rarr; Enable _all_ disabled alarms & timers - `Enable All` &rarr; Enable _all_ disabled alarms & timers

View File

@ -48,9 +48,10 @@ function showMainMenu() {
}; };
alarms.forEach((e, index) => { alarms.forEach((e, index) => {
var label = e.timer var label = (e.timer
? require("time_utils").formatDuration(e.timer) ? require("time_utils").formatDuration(e.timer)
: require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : ""); : (e.date ? `${e.date.substring(5,10)} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : ""))
) + (e.msg ? " " + e.msg : "");
menu[label] = { menu[label] = {
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff), value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index) onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
@ -67,11 +68,12 @@ function showNewMenu() {
"": { "title": /*LANG*/"New..." }, "": { "title": /*LANG*/"New..." },
"< Back": () => showMainMenu(), "< Back": () => showMainMenu(),
/*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined), /*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined),
/*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined) /*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined),
/*LANG*/"Event": () => showEditAlarmMenu(undefined, undefined, true)
}); });
} }
function showEditAlarmMenu(selectedAlarm, alarmIndex) { function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
var isNew = alarmIndex === undefined; var isNew = alarmIndex === undefined;
var alarm = require("sched").newDefaultAlarm(); var alarm = require("sched").newDefaultAlarm();
@ -82,11 +84,16 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
} }
var time = require("time_utils").decodeTime(alarm.t); var time = require("time_utils").decodeTime(alarm.t);
if (withDate && !alarm.date) alarm.date = new Date().toLocalISOString().slice(0,10);
var date = alarm.date ? new Date(alarm.date) : undefined;
var title = date ? (isNew ? /*LANG*/"New Event" : /*LANG*/"Edit Event") : (isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm");
var keyboard = "textinput";
try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
const menu = { const menu = {
"": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" }, "": { "title": title },
"< Back": () => { "< Back": () => {
prepareAlarmForSave(alarm, alarmIndex, time); prepareAlarmForSave(alarm, alarmIndex, time, date);
saveAndReload(); saveAndReload();
showMainMenu(); showMainMenu();
}, },
@ -106,6 +113,36 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
wrap: true, wrap: true,
onchange: v => time.m = v onchange: v => time.m = v
}, },
/*LANG*/"Day": {
value: date ? date.getDate() : null,
min: 1,
max: 31,
wrap: true,
onchange: v => date.setDate(v)
},
/*LANG*/"Month": {
value: date ? date.getMonth() + 1 : null,
format: v => require("date_utils").month(v),
onchange: v => date.setMonth((v+11)%12)
},
/*LANG*/"Year": {
value: date ? date.getFullYear() : null,
min: new Date().getFullYear(),
max: 2100,
onchange: v => date.setFullYear(v)
},
/*LANG*/"Message": {
value: alarm.msg,
onchange: () => {
setTimeout(() => {
keyboard.input({text:alarm.msg}).then(result => {
alarm.msg = result;
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
});
}, 100);
}
},
/*LANG*/"Enabled": { /*LANG*/"Enabled": {
value: alarm.on, value: alarm.on,
onchange: v => alarm.on = v onchange: v => alarm.on = v
@ -115,8 +152,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => { onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => {
alarm.rp = repeat; alarm.rp = repeat;
alarm.dow = dow; alarm.dow = dow;
alarm.t = require("time_utils").encodeTime(time); prepareAlarmForSave(alarm, alarmIndex, time, date, true);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex); setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
}) })
}, },
/*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v), /*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v),
@ -136,6 +173,15 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
} }
}; };
if (!keyboard) delete menu[/*LANG*/"Message"];
if (alarm.date || withDate) {
delete menu[/*LANG*/"Repeat"];
} else {
delete menu[/*LANG*/"Day"];
delete menu[/*LANG*/"Month"];
delete menu[/*LANG*/"Year"];
}
if (!isNew) { if (!isNew) {
menu[/*LANG*/"Delete"] = () => { menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => { E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
@ -145,7 +191,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
showMainMenu(); showMainMenu();
} else { } else {
alarm.t = require("time_utils").encodeTime(time); alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex); setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
} }
}); });
}; };
@ -154,14 +200,17 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
E.showMenu(menu); E.showMenu(menu);
} }
function prepareAlarmForSave(alarm, alarmIndex, time) { function prepareAlarmForSave(alarm, alarmIndex, time, date, temp) {
alarm.t = require("time_utils").encodeTime(time); alarm.t = require("time_utils").encodeTime(time);
alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0; alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0;
if(date) alarm.date = date.toLocalISOString().slice(0,10);
if (alarmIndex === undefined) { if(!temp) {
alarms.push(alarm); if (alarmIndex === undefined) {
} else { alarms.push(alarm);
alarms[alarmIndex] = alarm; } else {
alarms[alarmIndex] = alarm;
}
} }
} }
@ -255,6 +304,8 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
} }
var time = require("time_utils").decodeTime(timer.timer); var time = require("time_utils").decodeTime(timer.timer);
var keyboard = "textinput";
try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
const menu = { const menu = {
"": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" }, "": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" },
@ -285,6 +336,18 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
wrap: true, wrap: true,
onchange: v => time.s = v onchange: v => time.s = v
}, },
/*LANG*/"Message": {
value: timer.msg,
onchange: () => {
setTimeout(() => {
keyboard.input({text:timer.msg}).then(result => {
timer.msg = result;
prepareTimerForSave(timer, timerIndex, time, true);
setTimeout(showEditTimerMenu, 10, timer, timerIndex);
});
}, 100);
}
},
/*LANG*/"Enabled": { /*LANG*/"Enabled": {
value: timer.on, value: timer.on,
onchange: v => timer.on = v onchange: v => timer.on = v
@ -306,6 +369,7 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
} }
}; };
if (!keyboard) delete menu[/*LANG*/"Message"];
if (!isNew) { if (!isNew) {
menu[/*LANG*/"Delete"] = () => { menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => { E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
@ -324,15 +388,17 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
E.showMenu(menu); E.showMenu(menu);
} }
function prepareTimerForSave(timer, timerIndex, time) { function prepareTimerForSave(timer, timerIndex, time, temp) {
timer.timer = require("time_utils").encodeTime(time); timer.timer = require("time_utils").encodeTime(time);
timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer; timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer;
timer.last = 0; timer.last = 0;
if (timerIndex === undefined) { if (!temp) {
alarms.push(timer); if (timerIndex === undefined) {
} else { alarms.push(timer);
alarms[timerIndex] = timer; } else {
alarms[timerIndex] = timer;
}
} }
} }

View File

@ -2,17 +2,16 @@
"id": "alarm", "id": "alarm",
"name": "Alarms & Timers", "name": "Alarms & Timers",
"shortName": "Alarms", "shortName": "Alarms",
"version": "0.35", "version": "0.37",
"description": "Set alarms and timers on your Bangle", "description": "Set alarms and timers on your Bangle",
"icon": "app.png", "icon": "app.png",
"tags": "tool,alarm,widget", "tags": "tool,alarm",
"supports": [ "BANGLEJS", "BANGLEJS2" ], "supports": [ "BANGLEJS", "BANGLEJS2" ],
"readme": "README.md", "readme": "README.md",
"dependencies": { "scheduler":"type" }, "dependencies": { "scheduler":"type", "alarm":"widget" },
"storage": [ "storage": [
{ "name": "alarm.app.js", "url": "app.js" }, { "name": "alarm.app.js", "url": "app.js" },
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true }, { "name": "alarm.img", "url": "app-icon.js", "evaluate": true }
{ "name": "alarm.wid.js", "url": "widget.js" }
], ],
"screenshots": [ "screenshots": [
{ "url": "screenshot-1.png" }, { "url": "screenshot-1.png" },

View File

@ -16,4 +16,7 @@
0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152) 0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152)
0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge 0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge
0.18: Use new message library 0.18: Use new message library
If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged) If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged)
0.19: Add automatic translation for a couple of strings.
0.20: Fix wrong event used for forwarded GPS data from Gadgetbridge and add mapper to map longitude value correctly.
0.21: Fix broken 'Messages' button in menu

View File

@ -57,7 +57,7 @@
t:event.cmd=="incoming"?"add":"remove", t:event.cmd=="incoming"?"add":"remove",
id:"call", src:"Phone", id:"call", src:"Phone",
positive:true, negative:true, positive:true, negative:true,
title:event.name||"Call", body:"Incoming call\n"+event.number}); title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number});
require("messages").pushMessage(event); require("messages").pushMessage(event);
}, },
"alarm" : function() { "alarm" : function() {
@ -134,7 +134,11 @@
event.satellites = NaN; event.satellites = NaN;
event.course = NaN; event.course = NaN;
event.fix = 1; event.fix = 1;
Bangle.emit('gps', event); if (event.long!==undefined) {
event.lon = event.long;
delete event.long;
}
Bangle.emit('GPS', event);
}, },
"is_gps_active": function() { "is_gps_active": function() {
gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 }); gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
@ -148,7 +152,7 @@
Bangle.http = (url,options)=>{ Bangle.http = (url,options)=>{
options = options||{}; options = options||{};
if (!NRF.getSecurityStatus().connected) if (!NRF.getSecurityStatus().connected)
return Promise.reject("Not connected to Bluetooth"); return Promise.reject(/*LANG*/"Not connected to Bluetooth");
if (Bangle.httpRequest === undefined) if (Bangle.httpRequest === undefined)
Bangle.httpRequest={}; Bangle.httpRequest={};
if (options.id === undefined) { if (options.id === undefined) {
@ -208,7 +212,7 @@
// Replace set GPS power logic to suppress activation of gps (and instead request it from the phone) // Replace set GPS power logic to suppress activation of gps (and instead request it from the phone)
Bangle.setGPSPower = (isOn, appID) => { Bangle.setGPSPower = (isOn, appID) => {
// if not connected, use old logic // if not connected, use old logic
if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID); if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID);
// Emulate old GPS power logic // Emulate old GPS power logic
if (!Bangle._PWR) Bangle._PWR={}; if (!Bangle._PWR) Bangle._PWR={};
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[]; if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];

View File

@ -2,7 +2,7 @@
"id": "android", "id": "android",
"name": "Android Integration", "name": "Android Integration",
"shortName": "Android", "shortName": "Android",
"version": "0.18", "version": "0.21",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png", "icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge", "tags": "tool,system,messages,notifications,gadgetbridge",

View File

@ -1,6 +1,6 @@
(function(back) { (function(back) {
function gb(j) { function gb(j) {
Bluetooth.println(JSON.stringify(j)); Bluetooth.println(JSON.stringify(j));
@ -36,7 +36,7 @@
updateSettings(); updateSettings();
} }
}, },
/*LANG*/"Messages" : ()=>require("message").openGUI(), /*LANG*/"Messages" : ()=>require("messages").openGUI(),
}; };
E.showMenu(mainmenu); E.showMenu(mainmenu);
}) })

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Update to work with Bangle.js 2 0.02: Update to work with Bangle.js 2
0.03: Select GNSS systems to use for Bangle.js 2 0.03: Select GNSS systems to use for Bangle.js 2
0.04: Now turns GPS off after upload

View File

@ -133,7 +133,7 @@
function jsFromBase64(b64) { function jsFromBase64(b64) {
var bin = atob(b64); var bin = atob(b64);
var chunkSize = 128; var chunkSize = 128;
var js = "\x10Bangle.setGPSPower(1);\n"; // turn GPS on var js = "\x10Bangle.setGPSPower(1,'agps');\n"; // turn GPS on
if (isB1) { // UBLOX if (isB1) { // UBLOX
//js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`; //js += `\x10Bangle.on('GPS-raw',function (d) { if (d.startsWith("\\xB5\\x62\\x05\\x01")) Terminal.println("GPS ACK"); else if (d.startsWith("\\xB5\\x62\\x05\\x00")) Terminal.println("GPS NACK"); })\n`;
//js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec //js += "\x10var t=getTime()+1;while(t>getTime());\n"; // wait 1 sec
@ -158,6 +158,7 @@
var chunk = bin.substr(i,chunkSize); var chunk = bin.substr(i,chunkSize);
js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`; js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`;
} }
js = "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay
return js; return js;
} }

View File

@ -1,7 +1,7 @@
{ {
"id": "assistedgps", "id": "assistedgps",
"name": "Assisted GPS Updater (AGPS)", "name": "Assisted GPS Updater (AGPS)",
"version": "0.03", "version": "0.04",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.", "description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"sortorder": -1, "sortorder": -1,
"icon": "app.png", "icon": "app.png",

View File

@ -1,2 +1,4 @@
0.01: Create astrocalc app 0.01: Create astrocalc app
0.02: Store last GPS lock, can be used instead of waiting for new GPS on start 0.02: Store last GPS lock, can be used instead of waiting for new GPS on start
0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
0.04: Compatibility with Bangle.js 2, get location from My Location

View File

@ -9,10 +9,9 @@
* Calculate the Sun and Moon positions based on watch GPS and display graphically * Calculate the Sun and Moon positions based on watch GPS and display graphically
*/ */
const SunCalc = require("suncalc.js"); const SunCalc = require("suncalc"); // from modules folder
const storage = require("Storage"); const storage = require("Storage");
const LAST_GPS_FILE = "astrocalc.gps.json"; const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null);
function drawMoon(phase, x, y) { function drawMoon(phase, x, y) {
const moonImgFiles = [ const moonImgFiles = [
@ -73,7 +72,7 @@ function drawTitle(key) {
*/ */
function drawPoint(angle, radius, color) { function drawPoint(angle, radius, color) {
const pRad = Math.PI / 180; const pRad = Math.PI / 180;
const faceWidth = 80; // watch face radius const faceWidth = g.getWidth()/3; // watch face radius
const centerPx = g.getWidth() / 2; const centerPx = g.getWidth() / 2;
const a = angle * pRad; const a = angle * pRad;
@ -141,6 +140,7 @@ function drawData(title, obj, startX, startY) {
function drawMoonPositionPage(gps, title) { function drawMoonPositionPage(gps, title) {
const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon); const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon);
const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0};
const pageData = { const pageData = {
Azimuth: pos.azimuth.toFixed(2), Azimuth: pos.azimuth.toFixed(2),
@ -150,59 +150,61 @@ function drawMoonPositionPage(gps, title) {
}; };
const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
drawData(title, pageData, null, 80); drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20);
drawPoints(); drawPoints();
drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1}); drawPoint(azimuthDegrees, 8, moonColor);
let m = setWatch(() => { let m = setWatch(() => {
let m = moonIndexPageMenu(gps); let m = moonIndexPageMenu(gps);
}, BTN3, {repeat: false, edge: "falling"}); }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
} }
function drawMoonIlluminationPage(gps, title) { function drawMoonIlluminationPage(gps, title) {
const phaseNames = [ const phaseNames = [
"New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous", /*LANG*/"New Moon", /*LANG*/"Waxing Crescent", /*LANG*/"First Quarter", /*LANG*/"Waxing Gibbous",
"Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent", /*LANG*/"Full Moon", /*LANG*/"Waning Gibbous", /*LANG*/"Last Quater", /*LANG*/"Waning Crescent",
]; ];
const phase = SunCalc.getMoonIllumination(new Date()); const phase = SunCalc.getMoonIllumination(new Date());
const phaseIdx = Math.round(phase.phase*8);
const pageData = { const pageData = {
Phase: phaseNames[phase.phase], Phase: phaseNames[phaseIdx],
}; };
drawData(title, pageData, null, 35); drawData(title, pageData, null, 35);
drawMoon(phase.phase, g.getWidth() / 2, g.getHeight() / 2); drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2);
let m = setWatch(() => { let m = setWatch(() => {
let m = moonIndexPageMenu(gps); let m = moonIndexPageMenu(gps);
}, BTN3, {repease: false, edge: "falling"}); }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
} }
function drawMoonTimesPage(gps, title) { function drawMoonTimesPage(gps, title) {
const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon); const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon);
const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0};
const pageData = { const pageData = {
Rise: dateToTimeString(times.rise), Rise: dateToTimeString(times.rise),
Set: dateToTimeString(times.set), Set: dateToTimeString(times.set),
}; };
drawData(title, pageData, null, 105); drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
drawPoints(); drawPoints();
// Draw the moon rise position // Draw the moon rise position
const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon); const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI); const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI);
drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); drawPoint(riseAzimuthDegrees, 8, moonColor);
// Draw the moon set position // Draw the moon set position
const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon); const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); drawPoint(setAzimuthDegrees, 8, moonColor);
let m = setWatch(() => { let m = setWatch(() => {
let m = moonIndexPageMenu(gps); let m = moonIndexPageMenu(gps);
}, BTN3, {repease: false, edge: "falling"}); }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
} }
function drawSunShowPage(gps, key, date) { function drawSunShowPage(gps, key, date) {
@ -224,7 +226,7 @@ function drawSunShowPage(gps, key, date) {
Degrees: azimuthDegrees Degrees: azimuthDegrees
}; };
drawData(key, pageData, null, 85); drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
drawPoints(); drawPoints();
@ -233,7 +235,7 @@ function drawSunShowPage(gps, key, date) {
m = setWatch(() => { m = setWatch(() => {
m = sunIndexPageMenu(gps); m = sunIndexPageMenu(gps);
}, BTN3, {repeat: false, edge: "falling"}); }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
return null; return null;
} }
@ -273,15 +275,15 @@ function moonIndexPageMenu(gps) {
}, },
"Times": () => { "Times": () => {
m = E.showMenu(); m = E.showMenu();
drawMoonTimesPage(gps, "Times"); drawMoonTimesPage(gps, /*LANG*/"Times");
}, },
"Position": () => { "Position": () => {
m = E.showMenu(); m = E.showMenu();
drawMoonPositionPage(gps, "Position"); drawMoonPositionPage(gps, /*LANG*/"Position");
}, },
"Illumination": () => { "Illumination": () => {
m = E.showMenu(); m = E.showMenu();
drawMoonIlluminationPage(gps, "Illumination"); drawMoonIlluminationPage(gps, /*LANG*/"Illumination");
}, },
"< Back": () => m = indexPageMenu(gps), "< Back": () => m = indexPageMenu(gps),
}; };
@ -292,15 +294,15 @@ function moonIndexPageMenu(gps) {
function indexPageMenu(gps) { function indexPageMenu(gps) {
const menu = { const menu = {
"": { "": {
"title": "Select", "title": /*LANG*/"Select",
}, },
"Sun": () => { /*LANG*/"Sun": () => {
m = sunIndexPageMenu(gps); m = sunIndexPageMenu(gps);
}, },
"Moon": () => { /*LANG*/"Moon": () => {
m = moonIndexPageMenu(gps); m = moonIndexPageMenu(gps);
}, },
"< Exit": () => { load(); } "< Back": () => { load(); }
}; };
return E.showMenu(menu); return E.showMenu(menu);
@ -310,79 +312,10 @@ function getCenterStringX(str) {
return (g.getWidth() - g.stringWidth(str)) / 2; return (g.getWidth() - g.stringWidth(str)) / 2;
} }
/**
* GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page
*/
function drawGPSWaitPage() {
const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA=="));
const str1 = "Astrocalc v0.02";
const str2 = "Locating GPS";
const str3 = "Please wait...";
g.clear();
g.drawImage(img, 100, 50);
g.setFont("6x8", 1);
g.drawString(str1, getCenterStringX(str1), 105);
g.drawString(str2, getCenterStringX(str2), 140);
g.drawString(str3, getCenterStringX(str3), 155);
if (lastGPS) {
lastGPS = JSON.parse(lastGPS);
lastGPS.time = new Date();
const str4 = "Press Button 3 to use last GPS";
g.setColor("#d32e29");
g.fillRect(0, 190, g.getWidth(), 215);
g.setColor("#ffffff");
g.drawString(str4, getCenterStringX(str4), 200);
setWatch(() => {
clearWatch();
Bangle.setGPSPower(0);
m = indexPageMenu(lastGPS);
}, BTN3, {repeat: false});
}
g.flip();
const DEBUG = false;
if (DEBUG) {
clearWatch();
const gps = {
"lat": 56.45783133333,
"lon": -3.02188583333,
"alt": 75.3,
"speed": 0.070376,
"course": NaN,
"time":new Date(),
"satellites": 4,
"fix": 1
};
m = indexPageMenu(gps);
return;
}
Bangle.on('GPS', (gps) => {
if (gps.fix === 0) return;
clearWatch();
if (isNaN(gps.course)) gps.course = 0;
require("Storage").writeJSON(LAST_GPS_FILE, JSON.stringify(gps));
Bangle.setGPSPower(0);
Bangle.buzz();
Bangle.setLCDPower(true);
m = indexPageMenu(gps);
});
}
function init() { function init() {
Bangle.setGPSPower(1); let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
drawGPSWaitPage(); indexPageMenu(location);
} }
let m; let m;
init(); init();

View File

@ -1,15 +1,15 @@
{ {
"id": "astrocalc", "id": "astrocalc",
"name": "Astrocalc", "name": "Astrocalc",
"version": "0.02", "version": "0.04",
"description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", "description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app",
"icon": "astrocalc.png", "icon": "astrocalc.png",
"tags": "app,sun,moon,cycles,tool,outdoors", "tags": "app,sun,moon,cycles,tool",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true, "allow_emulator": true,
"dependencies": {"mylocation":"app"},
"storage": [ "storage": [
{"name":"astrocalc.app.js","url":"astrocalc-app.js"}, {"name":"astrocalc.app.js","url":"astrocalc-app.js"},
{"name":"suncalc.js","url":"suncalc.js"},
{"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true}, {"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true},
{"name":"first-quarter.img","url":"first-quarter-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":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true},

View File

@ -1,328 +0,0 @@
/*
(c) 2011-2015, Vladimir Agafonkin
SunCalc is a JavaScript library for calculating sun/moon position and light phases.
https://github.com/mourner/suncalc
*/
(function () { 'use strict';
// shortcuts for easier to read formulas
var PI = Math.PI,
sin = Math.sin,
cos = Math.cos,
tan = Math.tan,
asin = Math.asin,
atan = Math.atan2,
acos = Math.acos,
rad = PI / 180;
// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
// date/time constants and conversions
var dayMs = 1000 * 60 * 60 * 24,
J1970 = 2440588,
J2000 = 2451545;
function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
function fromJulian(j) { return (j + 0.5 - J1970) * dayMs; }
function toDays(date) { return toJulian(date) - J2000; }
// general calculations for position
var e = rad * 23.4397; // obliquity of the Earth
function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
function astroRefraction(h) {
if (h < 0) // the following formula works for positive altitudes only.
h = 0; // if h = -0.08901179 a div/0 would occur.
// formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
// 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
}
// general sun calculations
function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
function eclipticLongitude(M) {
var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
P = rad * 102.9372; // perihelion of the Earth
return M + C + P + PI;
}
function sunCoords(d) {
var M = solarMeanAnomaly(d),
L = eclipticLongitude(M);
return {
dec: declination(L, 0),
ra: rightAscension(L, 0)
};
}
var SunCalc = {};
// calculates sun position for a given date and latitude/longitude
SunCalc.getPosition = function (date, lat, lng) {
var lw = rad * -lng,
phi = rad * lat,
d = toDays(date),
c = sunCoords(d),
H = siderealTime(d, lw) - c.ra;
return {
azimuth: azimuth(H, phi, c.dec),
altitude: altitude(H, phi, c.dec)
};
};
// sun times configuration (angle, morning name, evening name)
var times = SunCalc.times = [
[-0.833, 'sunrise', 'sunset' ],
[ -0.3, 'sunriseEnd', 'sunsetStart' ],
[ -6, 'dawn', 'dusk' ],
[ -12, 'nauticalDawn', 'nauticalDusk'],
[ -18, 'nightEnd', 'night' ],
[ 6, 'goldenHourEnd', 'goldenHour' ]
];
// adds a custom time to the times config
SunCalc.addTime = function (angle, riseName, setName) {
times.push([angle, riseName, setName]);
};
// calculations for sun times
var J0 = 0.0009;
function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }
// returns set time for the given sun altitude
function getSetJ(h, lw, phi, dec, n, M, L) {
var w = hourAngle(h, phi, dec),
a = approxTransit(w, lw, n);
return solarTransitJ(a, M, L);
}
// calculates sun times for a given date, latitude/longitude, and, optionally,
// the observer height (in meters) relative to the horizon
SunCalc.getTimes = function (date, lat, lng, height) {
height = height || 0;
var lw = rad * -lng,
phi = rad * lat,
dh = observerAngle(height),
d = toDays(date),
n = julianCycle(d, lw),
ds = approxTransit(0, lw, n),
M = solarMeanAnomaly(ds),
L = eclipticLongitude(M),
dec = declination(L, 0),
Jnoon = solarTransitJ(ds, M, L),
i, len, time, h0, Jset, Jrise;
var result = {
solarNoon: new Date(fromJulian(Jnoon)),
nadir: new Date(fromJulian(Jnoon - 0.5))
};
for (i = 0, len = times.length; i < len; i += 1) {
time = times[i];
h0 = (time[0] + dh) * rad;
Jset = getSetJ(h0, lw, phi, dec, n, M, L);
Jrise = Jnoon - (Jset - Jnoon);
result[time[1]] = new Date(fromJulian(Jrise) - (dayMs / 2));
result[time[2]] = new Date(fromJulian(Jset) + (dayMs / 2));
}
return result;
};
// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
function moonCoords(d) { // geocentric ecliptic coordinates of the moon
var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
M = rad * (134.963 + 13.064993 * d), // mean anomaly
F = rad * (93.272 + 13.229350 * d), // mean distance
l = L + rad * 6.289 * sin(M), // longitude
b = rad * 5.128 * sin(F), // latitude
dt = 385001 - 20905 * cos(M); // distance to the moon in km
return {
ra: rightAscension(l, b),
dec: declination(l, b),
dist: dt
};
}
SunCalc.getMoonPosition = function (date, lat, lng) {
var lw = rad * -lng,
phi = rad * lat,
d = toDays(date),
c = moonCoords(d),
H = siderealTime(d, lw) - c.ra,
h = altitude(H, phi, c.dec),
// formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
h = h + astroRefraction(h); // altitude correction for refraction
return {
azimuth: azimuth(H, phi, c.dec),
altitude: h,
distance: c.dist,
parallacticAngle: pa
};
};
// calculations for illumination parameters of the moon,
// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
// Function updated from gist: https://gist.github.com/endel/dfe6bb2fbe679781948c
SunCalc.getMoonIllumination = function (date) {
let month = date.getMonth();
let year = date.getFullYear();
let day = date.getDate();
let c = 0;
let e = 0;
let jd = 0;
let b = 0;
if (month < 3) {
year--;
month += 12;
}
++month;
c = 365.25 * year;
e = 30.6 * month;
jd = c + e + day - 694039.09; // jd is total days elapsed
jd /= 29.5305882; // divide by the moon cycle
b = parseInt(jd); // int(jd) -> b, take integer part of jd
jd -= b; // subtract integer part to leave fractional part of original jd
b = Math.round(jd * 8); // scale fraction from 0-8 and round
if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0
return {phase: b};
};
function hoursLater(date, h) {
return new Date(date.valueOf() + h * dayMs / 24);
}
// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
var t = date;
if (inUTC) t.setUTCHours(0, 0, 0, 0);
else t.setHours(0, 0, 0, 0);
var hc = 0.133 * rad,
h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
for (var i = 1; i <= 24; i += 2) {
h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
a = (h0 + h2) / 2 - h1;
b = (h2 - h0) / 2;
xe = -b / (2 * a);
ye = (a * xe + b) * xe + h1;
d = b * b - 4 * a * h1;
roots = 0;
if (d >= 0) {
dx = Math.sqrt(d) / (Math.abs(a) * 2);
x1 = xe - dx;
x2 = xe + dx;
if (Math.abs(x1) <= 1) roots++;
if (Math.abs(x2) <= 1) roots++;
if (x1 < -1) x1 = x2;
}
if (roots === 1) {
if (h0 < 0) rise = i + x1;
else set = i + x1;
} else if (roots === 2) {
rise = i + (ye < 0 ? x2 : x1);
set = i + (ye < 0 ? x1 : x2);
}
if (rise && set) break;
h0 = h2;
}
var result = {};
if (rise) result.rise = hoursLater(t, rise);
if (set) result.set = hoursLater(t, set);
if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
return result;
};
// export as Node module / AMD module / browser variable
if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc;
else if (typeof define === 'function' && define.amd) define(SunCalc);
else global.SunCalc = SunCalc;
}());

View File

@ -14,3 +14,4 @@
0.14: Use ClockFace_menu.addItems 0.14: Use ClockFace_menu.addItems
0.15: Add Power saving option 0.15: Add Power saving option
0.16: Support Fast Loading 0.16: Support Fast Loading
0.17: Hide widgets instead of not loading them at all

View File

@ -1,7 +1,7 @@
{ {
"id": "barclock", "id": "barclock",
"name": "Bar Clock", "name": "Bar Clock",
"version": "0.16", "version": "0.17",
"description": "A simple digital clock showing seconds as a bar", "description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png", "icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],

View File

@ -1,5 +1,10 @@
(function(back) { (function(back) {
let s = require("Storage").readJSON("barclock.settings.json", true) || {}; let s = require("Storage").readJSON("barclock.settings.json", true) || {};
// migrate "don't load widgets" to "hide widgets"
if (!("hideWidgets" in s) && ("loadWidgets" in s) && !s.loadWidgets) {
s.hideWidgets = 1;
}
delete s.loadWidgets;
function save(key, value) { function save(key, value) {
s[key] = value; s[key] = value;
@ -19,7 +24,7 @@
}; };
let items = { let items = {
showDate: s.showDate, showDate: s.showDate,
loadWidgets: s.loadWidgets, hideWidgets: s.hideWidgets,
}; };
// Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway) // Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway)
if (process.env.HWVERSION>1) { if (process.env.HWVERSION>1) {

View File

@ -4,3 +4,5 @@
0.05: Update *on* the minute rather than every 15 secs 0.05: Update *on* the minute rather than every 15 secs
Now show widgets Now show widgets
Make compatible with themes, and Bangle.js 2 Make compatible with themes, and Bangle.js 2
0.06: Enable fastloading
0.07: Adds fullscreen mode setting

View File

@ -1,32 +1,41 @@
{
// Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr // Berlin Clock see https://en.wikipedia.org/wiki/Mengenlehreuhr
// https://github.com/eska-muc/BangleApps // https://github.com/eska-muc/BangleApps
var settings = require('Storage').readJSON("berlinc.json", true) || {};
const fields = [4, 4, 11, 4]; const fields = [4, 4, 11, 4];
const offset = 24;
const width = g.getWidth() - 2 * offset;
const height = g.getHeight() - 2 * offset;
const rowHeight = height / 4;
var show_date = false; let fullscreen = !!settings.fullscreen;
var show_time = false;
var yy = 0;
var rowlights = []; let show_date = false;
var time_digit = []; let show_time = false;
let yy = 0;
let rowlights = [];
let time_digit = [];
// timeout used to update every minute // timeout used to update every minute
var drawTimeout; let drawTimeout;
// schedule a draw for the next minute // schedule a draw for the next minute
function queueDraw() { let queueDraw = () => {
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() { drawTimeout = setTimeout(function() {
drawTimeout = undefined; drawTimeout = undefined;
draw(); draw();
}, 60000 - (Date.now() % 60000)); }, 60000 - (Date.now() % 60000));
} };
function draw() { let draw = () => {
g.reset().clearRect(0,24,g.getWidth(),g.getHeight()); let width = Math.min(Bangle.appRect.w,Bangle.appRect.h);
let height = width;
let offset = g.getHeight() - height;
let x = Math.floor((g.getWidth() - width)/2);
if (show_date) height -= 8;
let rowHeight = (height - 1) / 4;
g.setBgColor(g.theme.bg);
g.reset().clearRect(Bangle.appRect);
var now = new Date(); var now = new Date();
// show date below the clock // show date below the clock
@ -37,7 +46,7 @@ function draw() {
var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`; var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`;
var strWidth = g.stringWidth(dateString); var strWidth = g.stringWidth(dateString);
g.setColor(g.theme.fg).setFontAlign(-1,-1); g.setColor(g.theme.fg).setFontAlign(-1,-1);
g.drawString(dateString, ( g.getWidth() - strWidth ) / 2, height + offset + 4); g.drawString(dateString, ( Bangle.appRect.x + Bangle.appRect.w - strWidth ) / 2, Bangle.appRect.y2 - 5);
} }
rowlights[0] = Math.floor(now.getHours() / 5); rowlights[0] = Math.floor(now.getHours() / 5);
@ -50,15 +59,16 @@ function draw() {
time_digit[2] = Math.floor(now.getMinutes() / 10); time_digit[2] = Math.floor(now.getMinutes() / 10);
time_digit[3] = now.getMinutes() % 10; time_digit[3] = now.getMinutes() % 10;
g.drawRect(offset, offset, width + offset, height + offset); g.setColor(g.theme.fg);
g.drawRect(x, offset, x + width - 1, height + offset - 1);
for (row = 0; row < 4; row++) { for (row = 0; row < 4; row++) {
nfields = fields[row]; nfields = fields[row];
boxWidth = width / nfields; boxWidth = (width - 1) / nfields;
for (col = 0; col < nfields; col++) { for (col = 0; col < nfields; col++) {
x1 = col * boxWidth + offset; x1 = col * boxWidth + x;
y1 = row * rowHeight + offset; y1 = row * rowHeight + offset;
x2 = (col + 1) * boxWidth + offset; x2 = (col + 1) * boxWidth + x;
y2 = (row + 1) * rowHeight + offset; y2 = (row + 1) * rowHeight + offset;
g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2); g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2);
@ -84,33 +94,53 @@ function draw() {
queueDraw(); queueDraw();
} }
function toggleDate() { let toggleDate = () => {
show_date = ! show_date; show_date = ! show_date;
draw(); draw();
} }
function toggleTime() { let toggleTime = () => {
show_time = ! show_time; show_time = ! show_time;
draw(); draw();
} }
// Stop updates when LCD is off, restart when on let clear = () => {
Bangle.on('lcdPower',on=>{ if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
let onLcdPower = on => {
if (on) { if (on) {
draw(); // draw immediately, queue redraw draw(); // draw immediately, queue redraw
} else { // stop draw timer } else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout); clear();
drawTimeout = undefined;
} }
}); }
let cleanup = () => {
clear();
Bangle.removeListener("lcdPower", onLcdPower);
require("widget_utils").show();
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',onLcdPower);
// Show launcher when button pressed, handle up/down // Show launcher when button pressed, handle up/down
Bangle.setUI("clockupdown", dir=> { Bangle.setUI({mode: "clockupdown", remove: cleanup}, dir=> {
if (dir<0) toggleTime(); if (dir<0) toggleTime();
if (dir>0) toggleDate(); if (dir>0) toggleDate();
}); });
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
if (fullscreen){
if (process.env.HWVERSION == 2) require("widget_utils").swipeOn();
else require("widget_utils").hide();
}
Bangle.drawWidgets(); Bangle.drawWidgets();
draw(); draw();
}

View File

@ -1,7 +1,7 @@
{ {
"id": "berlinc", "id": "berlinc",
"name": "Berlin Clock", "name": "Berlin Clock",
"version": "0.05", "version": "0.07",
"description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)", "description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)",
"icon": "berlin-clock.png", "icon": "berlin-clock.png",
"type": "clock", "type": "clock",
@ -12,6 +12,8 @@
"screenshots": [{"url":"berlin-clock-screenshot.png"}], "screenshots": [{"url":"berlin-clock-screenshot.png"}],
"storage": [ "storage": [
{"name":"berlinc.app.js","url":"berlin-clock.js"}, {"name":"berlinc.app.js","url":"berlin-clock.js"},
{"name":"berlinc.settings.js","url":"settings.js"},
{"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true} {"name":"berlinc.img","url":"berlin-clock-icon.js","evaluate":true}
] ],
"data": [{"name":"berlinc.json"}]
} }

26
apps/berlinc/settings.js Normal file
View File

@ -0,0 +1,26 @@
(function(back) {
var FILE = "berlinc.json";
var settings = Object.assign({
fullscreem: false,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
var mainmenu = {
"": {
"title": "Berlin clock"
},
"< Back": () => back(),
"Fullscreen": {
value: !!settings.fullscreen,
onchange: v => {
settings.fullscreen = v;
writeSettings();
}
}
};
E.showMenu(mainmenu);
});

View File

@ -61,3 +61,6 @@
0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass 0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass
0.53: Add polyfills for pre-2v15.135 firmware for Bangle.load and Bangle.showClock 0.53: Add polyfills for pre-2v15.135 firmware for Bangle.load and Bangle.showClock
0.54: Fix for invalid version comparison in polyfill 0.54: Fix for invalid version comparison in polyfill
0.55: Add toLocalISOString polyfill for pre-2v15 firmwares
Only add boot info comments if settings.bootDebug was set
If settings.bootDebug is set, output timing for each section of .boot0

View File

@ -1,16 +1,22 @@
/* This rewrites boot0.js based on current settings. If settings changed then it /* This rewrites boot0.js based on current settings. If settings changed then it
recalculates, but this avoids us doing a whole bunch of reconfiguration most recalculates, but this avoids us doing a whole bunch of reconfiguration most
of the time. */ of the time. */
{ // execute in our own scope so we don't have to free variables...
E.showMessage(/*LANG*/"Updating boot0..."); E.showMessage(/*LANG*/"Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{}; let s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 const BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
var FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1")); const FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1"));
var boot = "", bootPost = ""; const DEBUG = s.bootDebug; // we can set this to enable debugging output in boot0
let boot = "", bootPost = "";
if (DEBUG) {
boot += "var _tm=Date.now()\n";
bootPost += "delete _tm;";
}
if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed
var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
} else { } else {
var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT); let CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT);
boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`;
} }
boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`;
@ -88,14 +94,25 @@ delete Bangle.showClock;
if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`; if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`;
delete Bangle.load; delete Bangle.load;
if (!Bangle.load) boot += `Bangle.load = load;\n`; if (!Bangle.load) boot += `Bangle.load = load;\n`;
let date = new Date();
delete date.toLocalISOString; // toLocalISOString was only introduced in 2v15
if (!date.toLocalISOString) boot += `Date.prototype.toLocalISOString = function() {
var o = this.getTimezoneOffset();
var d = new Date(this.getTime() - o*60000);
var sign = o>0?"-":"+";
o = Math.abs(o);
return d.toISOString().slice(0,-1)+sign+Math.floor(o/60).toString().padStart(2,0)+(o%60).toString().padStart(2,0);
};\n`;
// show timings
if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
// ================================================== BOOT.JS // ================================================== BOOT.JS
// Append *.boot.js files // Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed // These could change bleServices/bleServiceOptions if needed
var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
var getPriority = /.*\.(\d+)\.boot\.js$/; let getPriority = /.*\.(\d+)\.boot\.js$/;
var aPriority = a.match(getPriority); let aPriority = a.match(getPriority);
var bPriority = b.match(getPriority); let bPriority = b.match(getPriority);
if (aPriority && bPriority){ if (aPriority && bPriority){
return parseInt(aPriority[1]) - parseInt(bPriority[1]); return parseInt(aPriority[1]) - parseInt(bPriority[1]);
} else if (aPriority && !bPriority){ } else if (aPriority && !bPriority){
@ -106,14 +123,16 @@ var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
return a==b ? 0 : (a>b ? 1 : -1); return a==b ? 0 : (a>b ? 1 : -1);
}); });
// precalculate file size // precalculate file size
var fileSize = boot.length + bootPost.length; let fileSize = boot.length + bootPost.length;
bootFiles.forEach(bootFile=>{ bootFiles.forEach(bootFile=>{
// match the size of data we're adding below in bootFiles.forEach // match the size of data we're adding below in bootFiles.forEach
fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2; if (DEBUG) fileSize += 2+bootFile.length+1; // `//${bootFile}\n` comment
fileSize += require('Storage').read(bootFile).length+2; // boot code plus ";\n"
if (DEBUG) fileSize += 48+E.toJS(bootFile).length; // `print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`
}); });
// write file in chunks (so as not to use up all RAM) // write file in chunks (so as not to use up all RAM)
require('Storage').write('.boot0',boot,0,fileSize); require('Storage').write('.boot0',boot,0,fileSize);
var fileOffset = boot.length; let fileOffset = boot.length;
bootFiles.forEach(bootFile=>{ bootFiles.forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (function(){ ... }() // we add a semicolon so if the file is wrapped in (function(){ ... }()
// with no semicolon we don't end up with (function(){ ... }()(function(){ ... }() // with no semicolon we don't end up with (function(){ ... }()(function(){ ... }()
@ -122,16 +141,18 @@ bootFiles.forEach(bootFile=>{
// "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n"; // "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n";
// but we need to do this without ever loading everything into RAM as some // but we need to do this without ever loading everything into RAM as some
// boot files seem to be getting pretty big now. // boot files seem to be getting pretty big now.
require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset); if (DEBUG) {
fileOffset+=2+bootFile.length+1; require('Storage').write('.boot0',`//${bootFile}\n`,fileOffset);
var bf = require('Storage').read(bootFile); fileOffset+=2+bootFile.length+1;
}
let bf = require('Storage').read(bootFile);
// we can't just write 'bf' in one go because at least in 2v13 and earlier // we can't just write 'bf' in one go because at least in 2v13 and earlier
// Espruino wants to read the whole file into RAM first, and on Bangle.js 1 // Espruino wants to read the whole file into RAM first, and on Bangle.js 1
// it can be too big (especially BTHRM). // it can be too big (especially BTHRM).
var bflen = bf.length; let bflen = bf.length;
var bfoffset = 0; let bfoffset = 0;
while (bflen) { while (bflen) {
var bfchunk = Math.min(bflen, 2048); let bfchunk = Math.min(bflen, 2048);
require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset); require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset);
fileOffset+=bfchunk; fileOffset+=bfchunk;
bfoffset+=bfchunk; bfoffset+=bfchunk;
@ -139,15 +160,14 @@ bootFiles.forEach(bootFile=>{
} }
require('Storage').write('.boot0',";\n",fileOffset); require('Storage').write('.boot0',";\n",fileOffset);
fileOffset+=2; fileOffset+=2;
if (DEBUG) {
require('Storage').write('.boot0',`print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset);
fileOffset += 48+E.toJS(bootFile).length
}
}); });
require('Storage').write('.boot0',bootPost,fileOffset); require('Storage').write('.boot0',bootPost,fileOffset);
delete boot;
delete bootPost;
delete bootFiles;
delete fileSize;
delete fileOffset;
E.showMessage(/*LANG*/"Reloading..."); E.showMessage(/*LANG*/"Reloading...");
eval(require('Storage').read('.boot0')); }
// .bootcde should be run automatically after if required, since // .bootcde should be run automatically after if required, since
// we normally get called automatically from '.boot0' // we normally get called automatically from '.boot0'
eval(require('Storage').read('.boot0'));

View File

@ -1,7 +1,7 @@
{ {
"id": "boot", "id": "boot",
"name": "Bootloader", "name": "Bootloader",
"version": "0.54", "version": "0.55",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png", "icon": "bootloader.png",
"type": "bootloader", "type": "bootloader",

View File

@ -0,0 +1 @@
0.01: New App!

View File

@ -0,0 +1,9 @@
# BTHome Temperature and Pressure
This app displays temperature and pressure and advertises them over bluetooth using BTHome.io standard (along with battery level)
This can be used to integrate with [Home Assistant](https://www.home-assistant.io/), so you can use your Bangle as a wireless temperature/pressure sensor.
More info on the standard at https://bthome.io
And the data format used is https://bthome.io/format/

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4kA///1N6BIPf//1gMIwdE8sG2me+9Y/8C/2snXsoUNpdnzdt/xj/AH4AYgMRAAUQCyoYSCQNXs1muoFBFyHm1X//+qtwwPiMX1+YmczxP6uIwNFwN6yeDnGDmc504wNFwOpnGYC4OJweaGBsR9WTmYtBmc4GAOuC5ZGBt4SBAAQEBwf2JBcBiupnIuCmedxGTzVRC5cX1AuDnPZF4OKuIXLi3zIoedMgMzn9hC5uICQON5IDBxAXSznYC6RdDPQYXNO4JcB7pdCO56nBnGZ7p6DU5zXBXgSqDa5sAiPqIgOZd4c510RCxQXBi+pRQIXBxODzVxC5hIBvR1DnE505GMGAevzAvC/QuNGAfm1X//+qtwuOGAURq9ms11AoIWOGAQAEFw1EDBwWFggBCkUgAQMigUAAIIAJoABDCgIXQFwYXBCYYBDHAMCEAIkCFgcEAIIKCCoQFCkAhBAQIlCkAsBOoIXCBoIvEAwQTCAYI2BIwgXIF4YXDQwIVCC4YIBMIwfCAQRfGYBSPNC6TBFACgwBACouWAH4AiA="))

58
apps/bthometemp/app.js Normal file
View File

@ -0,0 +1,58 @@
// history of temperature/pressure readings
var history = [];
// When we get temperature...
function onTemperature(p) {
// Average the last 5 temperature readings
while (history.length>4) history.shift();
history.push(p);
var avrTemp = history.reduce((i,h)=>h.temperature+i,0) / history.length;
var avrPressure = history.reduce((i,h)=>h.pressure+i,0) / history.length;
var t = require('locale').temp(avrTemp).replace("'","°");
// Draw
var rect = Bangle.appRect;
g.reset(1).clearRect(rect.x, rect.y, rect.x2, rect.y2);
var x = (rect.x+rect.x2)/2;
var y = (rect.y+rect.y2)/2 + 10;
g.setFont("6x15").setFontAlign(0,0).drawString("Temperature:", x, y - 65);
g.setFontVector(50).setFontAlign(0,0).drawString(t, x, y-25);
g.setFont("6x15").setFontAlign(0,0).drawString("Pressure:", x, y+15 );
g.setFont("12x20").setFontAlign(0,0).drawString(Math.round(avrPressure)+" hPa", x, y+40);
// Set Bluetooth Advertising
// https://bthome.io/format/
var temp100 = Math.round(avrTemp*100);
var pressure100 = Math.round(avrPressure*100);
Bangle.bleAdvert[0xFCD2] = [ 0x40, /* BTHome Device Information
bit 0: "Encryption flag"
bit 1-4: "Reserved for future use"
bit 5-7: "BTHome Version" */
0x01, // Battery, 8 bit
E.getBattery(),
0x02, // Temperature, 16 bit
temp100&255,temp100>>8,
0x04, // Pressure, 16 bit
pressure100&255,(pressure100>>8)&255,pressure100>>16
];
NRF.setAdvertising(Bangle.bleAdvert);
}
// Gets the temperature in the most accurate way with pressure sensor
function drawTemperature() {
Bangle.getPressure().then(p =>{if (p) onTemperature(p);});
}
if (!Bangle.bleAdvert) Bangle.bleAdvert = {};
setInterval(function() {
drawTemperature();
}, 10000); // update every 10s
Bangle.loadWidgets();
Bangle.setUI({
mode : "custom",
back : function() {load();}
});
E.showMessage("Reading temperature...");
drawTemperature();

BIN
apps/bthometemp/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,14 @@
{ "id": "bthometemp",
"name": "BTHome Temperature and Pressure",
"shortName":"BTHome T",
"version":"0.01",
"description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
"icon": "app.png",
"tags": "bthome,bluetooth,temperature",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"bthometemp.app.js","url":"app.js"},
{"name":"bthometemp.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -40,3 +40,4 @@
0.16: Set powerdownRequested correctly on BTHRM power on 0.16: Set powerdownRequested correctly on BTHRM power on
Additional logging on errors Additional logging on errors
Add debug option for disabling active scanning Add debug option for disabling active scanning
0.17: New GUI based on layout library

View File

@ -1,5 +1,5 @@
var intervalInt; const BPM_FONT_SIZE="19%";
var intervalBt; const VALUE_TIMEOUT=3000;
var BODY_LOCS = { var BODY_LOCS = {
0: 'Other', 0: 'Other',
@ -7,46 +7,119 @@ var BODY_LOCS = {
2: 'Wrist', 2: 'Wrist',
3: 'Finger', 3: 'Finger',
4: 'Hand', 4: 'Hand',
5: 'Ear Lobe', 5: 'Earlobe',
6: 'Foot', 6: 'Foot',
};
var Layout = require("Layout");
function border(l,c) {
g.setColor(c).drawLine(l.x+l.w*0.05, l.y-4, l.x+l.w*0.95, l.y-4);
} }
function clear(y){ function getRow(id, text, additionalInfo){
g.reset(); let additional = [];
g.clearRect(0,y,g.getWidth(),y+75); let l = {
} type:"h", c: [
{
function draw(y, type, event) { type:"v",
clear(y); width: g.getWidth()*0.4,
var px = g.getWidth()/2; c: [
var str = event.bpm + ""; {type:"txt", halign:1, font:"8%", label:text, id:id+"text" },
g.reset(); {type:"txt", halign:1, font:BPM_FONT_SIZE, label:"--", id:id, bgCol: g.theme.bg }
g.setFontAlign(0,0); ]
g.setFontVector(40).drawString(str,px,y+20); },{
str = "Event: " + type; type:undefined, fillx:1
if (type === "HRM") { },{
str += " Confidence: " + event.confidence; type:"v",
g.setFontVector(12).drawString(str,px,y+40); valign: -1,
str = " Source: " + (event.src ? event.src : "internal"); width: g.getWidth()*0.45,
g.setFontVector(12).drawString(str,px,y+50); c: additional
},{
type:undefined, width:g.getWidth()*0.05
}
]
};
for (let i of additionalInfo){
let label = {type:"txt", font:"6x8", label:i + ":" };
let value = {type:"txt", font:"6x8", label:"--", id:id + i };
additional.push({type:"h", halign:-1, c:[ label, {type:undefined, fillx:1}, value ]});
} }
if (type === "BTHRM"){
if (event.battery) str += " Bat: " + (event.battery ? event.battery : ""); return l;
g.setFontVector(12).drawString(str,px,y+40); }
str= "";
if (event.location) str += "Loc: " + BODY_LOCS[event.location]; var layout = new Layout( {
if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(","); type:"v", c: [
g.setFontVector(12).drawString(str,px,y+50); getRow("int", "INT", ["Confidence"]),
str= ""; getRow("agg", "HRM", ["Confidence", "Source"]),
if (event.contact) str += " Contact: " + event.contact; getRow("bt", "BT", ["Battery","Location","Contact", "RR", "Energy"]),
if (event.energy) str += " kJoule: " + event.energy.toFixed(0); { type:undefined, height:8 } //dummy to protect debug output
g.setFontVector(12).drawString(str,px,y+60); ]
}, {
lazy:true
});
var int,agg,bt;
var firstEvent = true;
function draw(){
if (!(int || agg || bt)) return;
if (firstEvent) {
g.clearRect(Bangle.appRect);
firstEvent = false;
}
let now = Date.now();
if (int && int.time > (now - VALUE_TIMEOUT)){
layout.int.label = int.bpm;
if (!isNaN(int.confidence)) layout.intConfidence.label = int.confidence;
} else {
layout.int.label = "--";
layout.intConfidence.label = "--";
}
if (agg && agg.time > (now - VALUE_TIMEOUT)){
layout.agg.label = agg.bpm;
if (!isNaN(agg.confidence)) layout.aggConfidence.label = agg.confidence;
if (agg.src) layout.aggSource.label = agg.src;
} else {
layout.agg.label = "--";
layout.aggConfidence.label = "--";
layout.aggSource.label = "--";
}
if (bt && bt.time > (now - VALUE_TIMEOUT)) {
layout.bt.label = bt.bpm;
if (!isNaN(bt.battery)) layout.btBattery.label = bt.battery + "%";
if (bt.rr) layout.btRR.label = bt.rr.join(",");
if (!isNaN(bt.location)) layout.btLocation.label = BODY_LOCS[bt.location];
if (bt.contact !== undefined) layout.btContact.label = bt.contact ? "Yes":"No";
if (!isNaN(bt.energy)) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ";
} else {
layout.bt.label = "--";
layout.btBattery.label = "--";
layout.btRR.label = "--";
layout.btLocation.label = "--";
layout.btContact.label = "--";
layout.btEnergy.label = "--";
}
layout.update();
layout.render();
let first = true;
for (let c of layout.l.c){
if (first) {
first = false;
continue;
}
if (c.type && c.type == "h")
border(c,g.theme.fg);
} }
} }
var firstEventBt = true;
var firstEventInt = true;
// This can get called for the boot code to show what's happening // This can get called for the boot code to show what's happening
function showStatusInfo(txt) { function showStatusInfo(txt) {
@ -57,41 +130,26 @@ function showStatusInfo(txt) {
} }
function onBtHrm(e) { function onBtHrm(e) {
if (firstEventBt){ bt = e;
clear(24); bt.time = Date.now();
firstEventBt = false;
}
draw(100, "BTHRM", e);
if (e.bpm === 0){
Bangle.buzz(100,0.2);
}
if (intervalBt){
clearInterval(intervalBt);
}
intervalBt = setInterval(()=>{
clear(100);
}, 2000);
} }
function onHrm(e) { function onInt(e) {
if (firstEventInt){ int = e;
clear(24); int.time = Date.now();
firstEventInt = false;
}
draw(24, "HRM", e);
if (intervalInt){
clearInterval(intervalInt);
}
intervalInt = setInterval(()=>{
clear(24);
}, 2000);
} }
function onAgg(e) {
agg = e;
agg.time = Date.now();
}
var settings = require('Storage').readJSON("bthrm.json", true) || {}; var settings = require('Storage').readJSON("bthrm.json", true) || {};
Bangle.on('BTHRM', onBtHrm); Bangle.on('BTHRM', onBtHrm);
Bangle.on('HRM', onHrm); Bangle.on('HRM_int', onInt);
Bangle.on('HRM', onAgg);
Bangle.setHRMPower(1,'bthrm'); Bangle.setHRMPower(1,'bthrm');
if (!(settings.startWithHrm)){ if (!(settings.startWithHrm)){
@ -103,10 +161,11 @@ Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
if (Bangle.setBTHRMPower){ if (Bangle.setBTHRMPower){
g.reset().setFont("6x8",2).setFontAlign(0,0); g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 24); g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2);
setInterval(draw, 1000);
} else { } else {
g.reset().setFont("6x8",2).setFontAlign(0,0); g.reset().setFont("6x8",2).setFontAlign(0,0);
g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2 + 32); g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2);
} }
E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrm')); E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrm'));

View File

@ -553,14 +553,15 @@ exports.enable = () => {
if (settings.replace){ if (settings.replace){
// register a listener for original HRM events and emit as HRM_int // register a listener for original HRM events and emit as HRM_int
Bangle.on("HRM", (e) => { Bangle.on("HRM", (o) => {
e.modified = true; let e = Object.assign({},o);
log("Emitting HRM_int", e); log("Emitting HRM_int", e);
Bangle.emit("HRM_int", e); Bangle.emit("HRM_int", e);
if (fallbackActive){ if (fallbackActive){
// if fallback to internal HRM is active, emit as HRM_R to which everyone listens // if fallback to internal HRM is active, emit as HRM_R to which everyone listens
log("Emitting HRM_R(int)", e); o.src = "int";
Bangle.emit("HRM_R", e); log("Emitting HRM_R(int)", o);
Bangle.emit("HRM_R", o);
} }
}); });
@ -576,6 +577,13 @@ exports.enable = () => {
if (name == "HRM") o("HRM_R", cb); if (name == "HRM") o("HRM_R", cb);
else o(name, cb); else o(name, cb);
})(Bangle.removeListener); })(Bangle.removeListener);
} else {
Bangle.on("HRM", (o)=>{
o.src = "int";
let e = Object.assign({},o);
log("Emitting HRM_int", e);
Bangle.emit("HRM_int", e);
});
} }
Bangle.origSetHRMPower = Bangle.setHRMPower; Bangle.origSetHRMPower = Bangle.setHRMPower;

View File

@ -2,9 +2,10 @@
"id": "bthrm", "id": "bthrm",
"name": "Bluetooth Heart Rate Monitor", "name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM", "shortName": "BT HRM",
"version": "0.16", "version": "0.17",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screen.png"}],
"type": "app", "type": "app",
"tags": "health,bluetooth,hrm,bthrm", "tags": "health,bluetooth,hrm,bthrm",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],

BIN
apps/bthrm/screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -20,4 +20,13 @@
0.20: Better handling of async data such as getPressure. 0.20: Better handling of async data such as getPressure.
0.21: On the default menu the week of year can be shown. 0.21: On the default menu the week of year can be shown.
0.22: Use the new clkinfo module for the menu. 0.22: Use the new clkinfo module for the menu.
0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo. 0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo.
0.24: Update clock_info to avoid a redraw
0.25: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on fw2v16.
ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc.
0.26: Use clkinfo.addInteractive instead of a custom implementation
0.27: Clean out some leftovers in the remove function after switching to
clkinfo.addInteractive that would cause ReferenceError.
0.28: Option to show (1) time only and (2) week of year.
0.29: use setItem of clockInfoMenu to change the active item
0.30: Use widget_utils.

View File

@ -5,16 +5,12 @@ A very minimalistic clock.
## Features ## Features
The BW clock implements features that are exposed by other apps through the `clkinfo` module. The BW clock implements features that are exposed by other apps through the `clkinfo` module.
For example, if you install the HomeAssistant app, this menu item will be shown if you click right For example, if you install the HomeAssistant app, this menu item will be shown if you first
and additionally allows you to send triggers directly from the clock (select triggers via up/down and touch the bottom of the screen and then swipe left/right to the home assistant menu. To select
send via click center). Here are examples of other apps that are integrated: sub-items simply swipe up/down. To run an action (e.g. trigger home assistant), simply select the clkinfo (border) and touch on the item again. See also the screenshot below:
- Bangle data such as steps, heart rate, battery or charging state. ![](screenshot_3.png)
- Show agenda entries. A timer for an agenda entry can also be set by simply clicking in the middle of the screen. This can be used to not forget a meeting etc. Note that only one agenda-timer can be set at a time. *Requirement: Gadgetbridge calendar sync enabled*
- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app*
- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app*
Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden.
## Settings ## Settings
- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden). - Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden).
@ -22,25 +18,6 @@ Note: If some apps are not installed (e.gt. weather app), then this menu item is
- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further. - The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further.
- Your bangle uses the sys color settings so you can change the color too. - Your bangle uses the sys color settings so you can change the color too.
## Menu structure
2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to trigger HomeAssistant.
Simply click left / right to go through the menu entries such as Bangle, Weather etc.
and click up/down to move into this sub-menu. You can then click in the middle of the screen
to e.g. send a trigger via HomeAssistant once you selected it. The actions really depend
on the app that provide this sub-menu through the `clkinfo` module.
```
Bangle -- Agenda -- Weather -- HomeAssistant
| | | |
Battery Entry 1 Temperature Trigger1
| | | |
Steps ... ... ...
|
...
```
## Thanks to ## Thanks to
- Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located. - Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located.
- <a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a> - <a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>

View File

@ -1,10 +1,12 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears
/************************************************ /************************************************
* Includes * Includes
*/ */
const locale = require('locale'); const locale = require('locale');
const storage = require('Storage'); const storage = require('Storage');
const clock_info = require("clock_info"); const clock_info = require("clock_info");
const widget_utils = require("widget_utils");
/************************************************ /************************************************
* Globals * Globals
@ -12,8 +14,6 @@ const clock_info = require("clock_info");
const SETTINGS_FILE = "bwclk.setting.json"; const SETTINGS_FILE = "bwclk.setting.json";
const W = g.getWidth(); const W = g.getWidth();
const H = g.getHeight(); const H = g.getHeight();
var lock_input = false;
/************************************************ /************************************************
* Settings * Settings
@ -28,7 +28,20 @@ let settings = {
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) { for (const key in saved_settings) {
settings[key] = saved_settings[key] settings[key] = saved_settings[key];
}
let isFullscreen = function() {
var s = settings.screen.toLowerCase();
if(s == "dynamic"){
return Bangle.isLocked();
} else {
return s == "full";
}
};
let getLineY = function(){
return H/5*2 + (isFullscreen() ? 0 : 8);
} }
/************************************************ /************************************************
@ -74,32 +87,22 @@ Graphics.prototype.setMiniFont = function(scale) {
return this; return this;
}; };
function imgLock(){ let imgLock = function() {
return { return {
width : 16, height : 16, bpp : 1, width : 16, height : 16, bpp : 1,
transparent : 0, transparent : 0,
buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w="))
} };
} };
/************************************************ /************************************************
* Menu * Clock Info
*/ */
// Custom bwItems menu - therefore, its added here and not in a clkinfo.js file. let clockInfoItems = clock_info.load();
var bwItems = {
name: null,
img: null,
items: [
{ name: "WeekOfYear",
get: () => ({ text: "Week " + weekOfYear(), img: null}),
show: function() { bwItems.items[0].emit("redraw"); },
hide: function () {}
},
]
};
function weekOfYear() { // Add some custom clock-infos
let weekOfYear = function() {
var date = new Date(); var date = new Date();
date.setHours(0, 0, 0, 0); date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year. // Thursday in current week decides the year.
@ -111,87 +114,98 @@ function weekOfYear() {
- 3 + (week1.getDay() + 6) % 7) / 7); - 3 + (week1.getDay() + 6) % 7) / 7);
} }
clockInfoItems[0].items.unshift({ name : "weekofyear",
get : function() { return { text : "Week " + weekOfYear(),
img : null}},
show : function() {},
hide : function() {},
})
// Load menu // Empty for large time
var menu = clock_info.load(); clockInfoItems[0].items.unshift({ name : "nop",
menu = menu.concat(bwItems); get : function() { return { text : null,
img : null}},
show : function() {},
hide : function() {},
})
// Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it.
if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){
settings.menuPosX = 0;
settings.menuPosY = 0;
}
// Set draw functions for each item let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
menu.forEach((menuItm, x) => { x : 0,
menuItm.items.forEach((item, y) => { y: 135,
function drawItem() { w: W,
// For the clock, we have a special case, as we don't wanna redraw h: H-135,
// immediately when something changes. Instead, we update data each minute draw : (itm, info, options) => {
// to save some battery etc. Therefore, we hide (and disable the listener) var hideClkInfo = info.text == null;
// immedeately after redraw...
item.hide();
// After drawing the item, we enable inputs again... g.setColor(g.theme.fg);
lock_input = false; g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h);
var info = item.get(); g.setFontAlign(0,0);
drawMenuItem(info.text, info.img); g.setColor(g.theme.bg);
if (options.focus){
var y = hideClkInfo ? options.y+20 : options.y+2;
var h = hideClkInfo ? options.h-20 : options.h-2;
g.drawRect(options.x, y, options.x+options.w-2, y+h-1); // show if focused
g.drawRect(options.x+1, y+1, options.x+options.w-3, y+h-2); // show if focused
} }
item.on('redraw', drawItem); // In case we hide the clkinfo, we show the time again as the time should
}) // be drawn larger.
if(hideClkInfo){
drawTime();
return;
}
// Set text and font
var image = info.img;
var text = String(info.text);
if(text.split('\n').length > 1){
g.setMiniFont();
} else {
g.setSmallFont();
}
// Compute sizes
var strWidth = g.stringWidth(text);
var imgWidth = image == null ? 0 : 24;
var midx = options.x+options.w/2;
// Draw
if (image) {
var scale = imgWidth / image.width;
g.drawImage(image, midx-parseInt(imgWidth*1.3/2)-parseInt(strWidth/2), options.y+6, {scale: scale});
}
g.drawString(text, midx+parseInt(imgWidth*1.3/2), options.y+20);
// In case we are in focus and the focus box changes (fullscreen yes/no)
// we draw the time again. Otherwise it could happen that a while line is
// not cleared correctly.
if(options.focus) drawTime();
}
}); });
function canRunMenuItem(){
if(settings.menuPosY == 0){
return false;
}
var menuEntry = menu[settings.menuPosX];
var item = menuEntry.items[settings.menuPosY-1];
return item.run !== undefined;
}
function runMenuItem(){
if(settings.menuPosY == 0){
return;
}
var menuEntry = menu[settings.menuPosX];
var item = menuEntry.items[settings.menuPosY-1];
try{
var ret = item.run();
if(ret){
Bangle.buzz(300, 0.6);
}
} catch (ex) {
// Simply ignore it...
}
}
/************************************************ /************************************************
* Draw * Draw
*/ */
function draw() { let draw = function() {
// Queue draw again // Queue draw again
queueDraw(); queueDraw();
// Draw clock // Draw clock
drawDate(); drawDate();
drawMenuAndTime(); drawTime();
drawLock(); drawLock();
drawWidgets(); drawWidgets();
} };
function drawDate(){ let drawDate = function() {
// Draw background // Draw background
var y = H/5*2 + (isFullscreen() ? 0 : 8); var y = getLineY()
g.reset().clearRect(0,0,W,y); g.reset().clearRect(0,0,W,y);
// Draw date // Draw date
@ -216,17 +230,17 @@ function drawDate(){
g.setMediumFont(); g.setMediumFont();
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.drawString(dateStr, W/2 - fullDateW / 2, y+2); g.drawString(dateStr, W/2 - fullDateW / 2, y+2);
} };
function drawTime(y, smallText){ let drawTime = function() {
var hideClkInfo = clockInfoMenu.menuA == 0 && clockInfoMenu.menuB == 0;
// Draw background // Draw background
var y1 = getLineY();
var y = y1;
var date = new Date(); var date = new Date();
// Draw time
g.setColor(g.theme.bg);
g.setFontAlign(0,0);
var hours = String(date.getHours()); var hours = String(date.getHours());
var minutes = date.getMinutes(); var minutes = date.getMinutes();
minutes = minutes < 10 ? String("0") + minutes : minutes; minutes = minutes < 10 ? String("0") + minutes : minutes;
@ -236,212 +250,93 @@ function drawTime(y, smallText){
// Set y coordinates correctly // Set y coordinates correctly
y += parseInt((H - y)/2) + 5; y += parseInt((H - y)/2) + 5;
// Show large or small time depending on info entry if (hideClkInfo){
if(smallText){ g.setLargeFont();
} else {
y -= 15; y -= 15;
g.setMediumFont(); g.setMediumFont();
} else {
g.setLargeFont();
} }
g.drawString(timeStr, W/2, y);
}
function drawMenuItem(text, image){
// First clear the time region
var y = H/5*2 + (isFullscreen() ? 0 : 8);
// Clear region and draw time
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.fillRect(0,y,W,H); g.fillRect(0,y1,W,y+20 + (hideClkInfo ? 1 : 0) + (isFullscreen() ? 3 : 0));
// Draw menu text g.setColor(g.theme.bg);
var hasText = (text != null && text != ""); g.setFontAlign(0,0);
if(hasText){ g.drawString(timeStr, W/2, y);
g.setFontAlign(0,0); };
// For multiline text we show an even smaller font...
text = String(text);
if(text.split('\n').length > 1){
g.setMiniFont();
} else {
g.setSmallFont();
}
var imgWidth = image == null ? 0 : 24;
var strWidth = g.stringWidth(text);
g.setColor(g.theme.fg).fillRect(0, 149-14, W, H);
g.setColor(g.theme.bg).drawString(text, W/2 + imgWidth/2 + 2, 149+3);
if(image != null){
var scale = imgWidth / image.width;
g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 149 - parseInt(imgWidth/2), {scale: scale});
}
}
// Draw time
drawTime(y, hasText);
}
function drawMenuAndTime(){ let drawLock = function() {
var menuEntry = menu[settings.menuPosX];
// The first entry is the overview...
if(settings.menuPosY == 0){
drawMenuItem(menuEntry.name, menuEntry.img);
return;
}
// Draw item if needed
lock_input = true;
var item = menuEntry.items[settings.menuPosY-1];
item.show();
}
function drawLock(){
if(settings.showLock && Bangle.isLocked()){ if(settings.showLock && Bangle.isLocked()){
g.setColor(g.theme.fg); g.setColor(g.theme.fg);
g.drawImage(imgLock(), W-16, 2); g.drawImage(imgLock(), W-16, 2);
} }
} };
function drawWidgets(){ let drawWidgets = function() {
if(isFullscreen()){ if(isFullscreen()){
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} widget_utils.hide();
} else { } else {
Bangle.drawWidgets(); Bangle.drawWidgets();
} }
} };
function isFullscreen(){
var s = settings.screen.toLowerCase();
if(s == "dynamic"){
return Bangle.isLocked()
} else {
return s == "full"
}
}
/************************************************ /************************************************
* Listener * Listener
*/ */
// timeout used to update every minute // timeout used to update every minute
var drawTimeout; let drawTimeout;
// schedule a draw for the next minute // schedule a draw for the next minute
function queueDraw() { let queueDraw = function() {
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() { drawTimeout = setTimeout(function() {
drawTimeout = undefined; drawTimeout = undefined;
draw(); draw();
}, 60000 - (Date.now() % 60000)); }, 60000 - (Date.now() % 60000));
} };
// Stop updates when LCD is off, restart when on // Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{ let lcdListenerBw = function(on) {
if (on) { if (on) {
draw(); // draw immediately, queue redraw draw(); // draw immediately, queue redraw
} else { // stop draw timer } else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined; drawTimeout = undefined;
} }
}); };
Bangle.on('lcdPower', lcdListenerBw);
Bangle.on('lock', function(isLocked) { let lockListenerBw = function(isLocked) {
if (drawTimeout) clearTimeout(drawTimeout); if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined; drawTimeout = undefined;
if(!isLocked && settings.screen.toLowerCase() == "dynamic"){ if(!isLocked && settings.screen.toLowerCase() == "dynamic"){
// If we have to show the widgets again, we load it from our // If we have to show the widgets again, we load it from our
// cache and not through Bangle.loadWidgets as its much faster! // cache and not through Bangle.loadWidgets as its much faster!
for (let wd of WIDGETS) {wd.draw=wd._draw;wd.area=wd._area;} widget_utils.show();
} }
draw(); draw();
}); };
Bangle.on('lock', lockListenerBw);
Bangle.on('charging',function(charging) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
let charging = function(charging){
// Jump to battery // Jump to battery
settings.menuPosX = 0; clockInfoMenu.setItem(0, 2);
settings.menuPosY = 1; drawTime();
draw(); }
}); Bangle.on('charging', charging);
Bangle.on('touch', function(btn, e){
var widget_size = isFullscreen() ? 0 : 20; // Its not exactly 24px -- empirically it seems that 20 worked better...
var left = parseInt(g.getWidth() * 0.22);
var right = g.getWidth() - left;
var upper = parseInt(g.getHeight() * 0.22) + widget_size;
var lower = g.getHeight() - upper;
var is_upper = e.y < upper;
var is_lower = e.y > lower;
var is_left = e.x < left && !is_upper && !is_lower;
var is_right = e.x > right && !is_upper && !is_lower;
var is_center = !is_upper && !is_lower && !is_left && !is_right;
if(lock_input){
return;
}
if(is_lower){
Bangle.buzz(40, 0.6);
settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1);
drawMenuAndTime();
}
if(is_upper){
if(e.y < widget_size){
return;
}
Bangle.buzz(40, 0.6);
settings.menuPosY = settings.menuPosY-1;
settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY;
drawMenuAndTime();
}
if(is_right){
Bangle.buzz(40, 0.6);
settings.menuPosX = (settings.menuPosX+1) % menu.length;
settings.menuPosY = 0;
drawMenuAndTime();
}
if(is_left){
Bangle.buzz(40, 0.6);
settings.menuPosY = 0;
settings.menuPosX = settings.menuPosX-1;
settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
drawMenuAndTime();
}
if(is_center){
if(canRunMenuItem()){
runMenuItem();
}
}
});
E.on("kill", function(){
try{
storage.write(SETTINGS_FILE, settings);
} catch(ex){
// If this fails, we still kill the app...
}
});
let kill = function(){
clockInfoMenu.remove();
delete clockInfoMenu;
};
E.on("kill", kill);
/************************************************ /************************************************
* Startup Clock * Startup Clock
@ -450,17 +345,31 @@ E.on("kill", function(){
// The upper part is inverse i.e. light if dark and dark if light theme // The upper part is inverse i.e. light if dark and dark if light theme
// is enabled. In order to draw the widgets correctly, we invert the // is enabled. In order to draw the widgets correctly, we invert the
// dark/light theme as well as the colors. // dark/light theme as well as the colors.
let themeBackup = g.theme;
g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear(); g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear();
// Show launcher when middle button pressed // Show launcher when middle button pressed
Bangle.setUI("clock"); Bangle.setUI({
mode : "clock",
remove : function() {
// Called to unload all of the clock app
Bangle.removeListener('lcdPower', lcdListenerBw);
Bangle.removeListener('lock', lockListenerBw);
Bangle.removeListener('charging', charging);
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
// save settings
kill();
E.removeListener("kill", kill);
g.setTheme(themeBackup);
widget_utils.show();
}
});
// Load widgets and draw clock the first time // Load widgets and draw clock the first time
Bangle.loadWidgets(); Bangle.loadWidgets();
// Cache draw function for dynamic screen to hide / show widgets
// Bangle.loadWidgets() could also be called later on but its much slower!
for (let wd of WIDGETS) {wd._draw=wd.draw; wd._area=wd.area;}
// Draw first time // Draw first time
draw(); draw();
} // End of app scope

View File

@ -1,11 +1,11 @@
{ {
"id": "bwclk", "id": "bwclk",
"name": "BW Clock", "name": "BW Clock",
"version": "0.23", "version": "0.30",
"description": "A very minimalistic clock to mainly show date and time.", "description": "A very minimalistic clock.",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}, {"url":"screenshot_4.png"}], "screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}],
"type": "clock", "type": "clock",
"tags": "clock,clkinfo", "tags": "clock,clkinfo",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -3,3 +3,4 @@
0.03: Tell clock widgets to hide. 0.03: Tell clock widgets to hide.
0.04: Improve current time readability in light theme. 0.04: Improve current time readability in light theme.
0.05: Show calendar colors & improved all day events. 0.05: Show calendar colors & improved all day events.
0.06: Improved multi-line locations & titles

View File

@ -54,13 +54,15 @@ function drawEventBody(event, y) {
var yStart = y; var yStart = y;
if (lines.length > 2) { if (lines.length > 2) {
lines = lines.slice(0,2); lines = lines.slice(0,2);
lines[1] = lines[1].slice(0,-3)+"..."; lines[1] += "...";
} }
g.drawString(lines.join('\n'),10,y); g.drawString(lines.join('\n'),10,y);
y+=20 * lines.length; y+=20 * lines.length;
if(event.location) { if(event.location) {
g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),10,y); g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),10,y);
g.drawString(event.location,25,y); var loclines = g.wrapString(event.location, g.getWidth()-30);
if(loclines.length>1) loclines[0] += "...";
g.drawString(loclines[0],25,y);
y+=20; y+=20;
} }
if (event.color) { if (event.color) {
@ -131,4 +133,3 @@ var minuteInterval = setInterval(redraw, 60 * 1000);
Bangle.setUI("clock"); Bangle.setUI("clock");
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();

View File

@ -2,7 +2,7 @@
"id": "calclock", "id": "calclock",
"name": "Calendar Clock", "name": "Calendar Clock",
"shortName": "CalClock", "shortName": "CalClock",
"version": "0.05", "version": "0.06",
"description": "Show the current and upcoming events synchronized from Gadgetbridge", "description": "Show the current and upcoming events synchronized from Gadgetbridge",
"icon": "calclock.png", "icon": "calclock.png",
"type": "clock", "type": "clock",

View File

@ -1,3 +1,9 @@
0.01: New App! 0.01: New App!
0.02: Support Bangle.js 2 0.02: Support Bangle.js 2
0.03: Fix bug for Bangle.js 2 where g.flip was not being called. 0.03: Fix bug for Bangle.js 2 where g.flip was not being called.
0.04: Combine code for both apps
Better colors for Bangle.js 2
Fix selection animation for Bangle.js 2
New icon
Slightly wider arc segments for better visibility
Extract arc drawing code in library

View File

@ -11,16 +11,21 @@ the players seated in a circle, set the number of segments equal to the number
of players, ensure that each person knows which colour represents them, and then of players, ensure that each person knows which colour represents them, and then
choose a segment. After a short animation, the chosen segment will fill the screen. choose a segment. After a short animation, the chosen segment will fill the screen.
You can use Choozi to randomly select an element from any set with 2 to 13 members, You can use Choozi to randomly select an element from any set with 2 to 15 members,
as long as you can define a bijection between members of the set and coloured as long as you can define a bijection between members of the set and coloured
segments on the Bangle.js display. segments on the Bangle.js display.
## Controls ## Controls Bangle 1
BTN1: increase the number of segments BTN1: increase the number of segments
BTN2: choose a segment at random BTN2: choose a segment at random
BTN3: decrease the number of segments BTN3: decrease the number of segments
## Controls Bangle 2
Swipe up/down: increase/decrease the number of segments
BTN1 or tap: choose a segment at random
## Creator ## Creator
James Stanley James Stanley

View File

@ -1 +1 @@
require("heatshrink").decompress(atob("mEwggLIrnM4uqAAIhPgvMAAPFzIABzWgCxkMCweqC4QABDBYtC5QVFDBoWCCo5KLOQIWKDARFICxhJIFwOpC5owFFyAwGUYIuOGAwuRC4guSJAgXBCyIwDIyQXF5IXSzJeVMAReUAAOQhheTMAVcC6yOUC4aOUC7GZUyoXXzWqhQXVxGqC9mYC7OqC9eoxEKC6uBC6uIwAXBPCSmBwEAC6Z2BiAXBJCR2BgEAjQXSlGBC4JgSLwYABJCJGBLwJIDGB+IIwRIDGByNBIwZIDGBhdBRoQwSLoIuFGAYYKCwIuGGAgYI1QWBRgYYJMYmaFoSMEAAyrBAAgVCCxgYGjAWQAAMBC4UILZQA==")) require("heatshrink").decompress(atob("mEwwcH/4AW/u27dt2wQL/YOBCIXbv4QI+AODAQVsh4RHwEbCI0LCI9gCIOANAXbsFbG437tkDPg1btoRFFoILBgmSpMggECHQO/CAf2CIVJkgRBAQIjC24RFsECCItIgIRFMYMAiQRFpMAlqmDVwPYgAOEAQUggu274RD4BWCCIskCIPbCIPt20ABwwCCwARFgIRJyEWCIVt2EJCJi2BCJmSUgIRCwARNt/7CIIOICI1sWAwCFoFbCOtt8EACJsAgARR8hwBCJlJk4RlgARQAgIRKDwMn/gRBdJgRPyARBn4RBpARLiQRB/4RBgIRJwAREpIRLAYP///ypMgCJMACI0ECI4JCp4RB/wZECIsAAYN/CIP/5JPDCIhjDCIraHTIWTCAX//K7DCI+fCIf/EZA1CCAn//ipCLIsBk4RF/5ZHCIIQG//wPo8vCI//6QRFpYQIAAPpCIeXCBQAC/VfBI4="))

View File

@ -4,15 +4,16 @@
* *
* James Stanley 2021 * James Stanley 2021
*/ */
const GU = require("graphics_utils");
var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; var colours = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff', '#ffffff'];
var colours2 = ['#808080', '#404040', '#000040', '#004000', '#400000', '#ff8000', '#804000', '#4000c0'];
var stepAngle = 0.18; // radians - resolution of polygon var stepAngle = 0.18; // radians - resolution of polygon
var gapAngle = 0.035; // radians - gap between segments var gapAngle = 0.035; // radians - gap between segments
var perimMin = 110; // px - min. radius of perimeter var perimMin = g.getWidth()*0.40; // px - min. radius of perimeter
var perimMax = 120; // px - max. radius of perimeter var perimMax = g.getWidth()*0.49; // px - max. radius of perimeter
var segmentMax = 106; // px - max radius of filled-in segment var segmentMax = g.getWidth()*0.38; // px - max radius of filled-in segment
var segmentStep = 5; // px - step size of segment fill animation var segmentStep = 5; // px - step size of segment fill animation
var circleStep = 4; // px - step size of circle fill animation var circleStep = 4; // px - step size of circle fill animation
@ -22,10 +23,10 @@ var minSpeed = 0.001; // rad/sec
var animStartSteps = 300; // how many steps before it can start slowing? var animStartSteps = 300; // how many steps before it can start slowing?
var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
var ballSize = 3; // px - ball radius var ballSize = 3; // px - ball radius
var ballTrack = 100; // px - radius of ball path var ballTrack = perimMin - ballSize*2; // px - radius of ball path
var centreX = 120; // px - centre of screen var centreX = g.getWidth()*0.5; // px - centre of screen
var centreY = 120; // px - centre of screen var centreY = g.getWidth()*0.5; // px - centre of screen
var fontSize = 50; // px var fontSize = 50; // px
@ -33,7 +34,6 @@ var radians = 2*Math.PI; // radians per circle
var defaultN = 3; // default value for N var defaultN = 3; // default value for N
var minN = 2; var minN = 2;
var maxN = colours.length;
var N; var N;
var arclen; var arclen;
@ -51,42 +51,14 @@ function shuffle (array) {
} }
} }
// draw an arc between radii minR and maxR, and between
// angles minAngle and maxAngle
function arc(minR, maxR, minAngle, maxAngle) {
var step = stepAngle;
var angle = minAngle;
var inside = [];
var outside = [];
var c, s;
while (angle < maxAngle) {
c = Math.cos(angle);
s = Math.sin(angle);
inside.push(centreX+c*minR); // x
inside.push(centreY+s*minR); // y
// outside coordinates are built up in reverse order
outside.unshift(centreY+s*maxR); // y
outside.unshift(centreX+c*maxR); // x
angle += step;
}
c = Math.cos(maxAngle);
s = Math.sin(maxAngle);
inside.push(centreX+c*minR);
inside.push(centreY+s*minR);
outside.unshift(centreY+s*maxR);
outside.unshift(centreX+c*maxR);
var vertices = inside.concat(outside);
g.fillPoly(vertices, true);
}
// draw the arc segments around the perimeter // draw the arc segments around the perimeter
function drawPerimeter() { function drawPerimeter() {
g.setBgColor('#000000');
g.clear(); g.clear();
for (var i = 0; i < N; i++) { for (var i = 0; i < N; i++) {
g.setColor(colours[i%colours.length]); g.setColor(colours[i%colours.length]);
var minAngle = (i/N)*radians; var minAngle = (i/N)*radians;
arc(perimMin,perimMax,minAngle,minAngle+arclen); GU.fillArc(g, centreX, centreY, perimMin,perimMax,minAngle,minAngle+arclen, stepAngle);
} }
} }
@ -131,6 +103,7 @@ function animateChoice(target) {
g.fillCircle(x, y, ballSize); g.fillCircle(x, y, ballSize);
oldx=x; oldx=x;
oldy=y; oldy=y;
if (process.env.HWVERSION == 2) g.flip();
} }
} }
@ -141,11 +114,15 @@ function choose() {
var maxAngle = minAngle + arclen; var maxAngle = minAngle + arclen;
animateChoice((minAngle+maxAngle)/2); animateChoice((minAngle+maxAngle)/2);
g.setColor(colours[chosen%colours.length]); g.setColor(colours[chosen%colours.length]);
for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep){
arc(i, perimMax, minAngle, maxAngle); GU.fillArc(g, centreX, centreY, i, perimMax, minAngle, maxAngle, stepAngle);
arc(0, perimMax, minAngle, maxAngle); if (process.env.HWVERSION == 2) g.flip();
for (var r = 1; r < segmentMax; r += circleStep) }
GU.fillArc(g, centreX, centreY, 0, perimMax, minAngle, maxAngle, stepAngle);
for (var r = 1; r < segmentMax; r += circleStep){
g.fillCircle(centreX,centreY,r); g.fillCircle(centreX,centreY,r);
if (process.env.HWVERSION == 2) g.flip();
}
g.fillCircle(centreX,centreY,segmentMax); g.fillCircle(centreX,centreY,segmentMax);
} }
@ -171,38 +148,47 @@ function setN(n) {
drawPerimeter(); drawPerimeter();
} }
// save N to choozi.txt // save N to choozi.save
function writeN() { function writeN() {
var file = require("Storage").open("choozi.txt","w"); var savedN = read();
file.write(N); if (savedN != N) require("Storage").write("choozi.save","" + N);
} }
// load N from choozi.txt function read(){
var n = require("Storage").read("choozi.save");
if (n !== undefined) return parseInt(n);
return defaultN;
}
// load N from choozi.save
function readN() { function readN() {
var file = require("Storage").open("choozi.txt","r"); setN(read());
var n = file.readLine();
if (n !== undefined) setN(parseInt(n));
else setN(defaultN);
} }
shuffle(colours); // is this really best? if (process.env.HWVERSION == 1){
Bangle.setLCDMode("direct"); colours=colours.concat(colours2);
Bangle.setLCDTimeout(0); // keep screen on shuffle(colours);
} else {
shuffle(colours);
shuffle(colours2);
colours=colours.concat(colours2);
}
var maxN = colours.length;
if (process.env.HWVERSION == 1){
Bangle.setLCDMode("direct");
Bangle.setLCDTimeout(0); // keep screen on
}
readN(); readN();
drawN(); drawN();
setWatch(() => { Bangle.setUI("updown", (v)=>{
setN(N+1); if (!v){
drawN(); writeN();
}, BTN1, {repeat:true}); drawPerimeter();
choose();
setWatch(() => { } else {
writeN(); setN(N-v);
drawPerimeter(); drawN();
choose(); }
}, BTN2, {repeat:true}); });
setWatch(() => {
setN(N-1);
drawN();
}, BTN3, {repeat:true});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 551 B

View File

@ -1,207 +0,0 @@
/* Choozi - Choose people or things at random using Bangle.js.
* Inspired by the "Chwazi" Android app
*
* James Stanley 2021
*/
var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff'];
var stepAngle = 0.18; // radians - resolution of polygon
var gapAngle = 0.035; // radians - gap between segments
var perimMin = 80; // px - min. radius of perimeter
var perimMax = 87; // px - max. radius of perimeter
var segmentMax = 70; // px - max radius of filled-in segment
var segmentStep = 5; // px - step size of segment fill animation
var circleStep = 4; // px - step size of circle fill animation
// rolling ball animation:
var maxSpeed = 0.08; // rad/sec
var minSpeed = 0.001; // rad/sec
var animStartSteps = 300; // how many steps before it can start slowing?
var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
var ballSize = 3; // px - ball radius
var ballTrack = 75; // px - radius of ball path
var centreX = 88; // px - centre of screen
var centreY = 88; // px - centre of screen
var fontSize = 50; // px
var radians = 2*Math.PI; // radians per circle
var defaultN = 3; // default value for N
var minN = 2;
var maxN = colours.length;
var N;
var arclen;
// https://www.frankmitchell.org/2015/01/fisher-yates/
function shuffle (array) {
var i = 0
, j = 0
, temp = null;
for (i = array.length - 1; i > 0; i -= 1) {
j = Math.floor(Math.random() * (i + 1));
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// draw an arc between radii minR and maxR, and between
// angles minAngle and maxAngle
function arc(minR, maxR, minAngle, maxAngle) {
var step = stepAngle;
var angle = minAngle;
var inside = [];
var outside = [];
var c, s;
while (angle < maxAngle) {
c = Math.cos(angle);
s = Math.sin(angle);
inside.push(centreX+c*minR); // x
inside.push(centreY+s*minR); // y
// outside coordinates are built up in reverse order
outside.unshift(centreY+s*maxR); // y
outside.unshift(centreX+c*maxR); // x
angle += step;
}
c = Math.cos(maxAngle);
s = Math.sin(maxAngle);
inside.push(centreX+c*minR);
inside.push(centreY+s*minR);
outside.unshift(centreY+s*maxR);
outside.unshift(centreX+c*maxR);
var vertices = inside.concat(outside);
g.fillPoly(vertices, true);
}
// draw the arc segments around the perimeter
function drawPerimeter() {
g.clear();
for (var i = 0; i < N; i++) {
g.setColor(colours[i%colours.length]);
var minAngle = (i/N)*radians;
arc(perimMin,perimMax,minAngle,minAngle+arclen);
}
}
// animate a ball rolling around and settling at "target" radians
function animateChoice(target) {
var angle = 0;
var speed = 0;
var oldx = -10;
var oldy = -10;
var decelFromAngle = -1;
var allowDecel = false;
for (var i = 0; true; i++) {
angle = angle + speed;
if (angle > radians) angle -= radians;
if (i < animStartSteps || (speed < maxSpeed && !allowDecel)) {
speed = speed + accel;
if (speed > maxSpeed) {
speed = maxSpeed;
/* when we reach max speed, we know how long it takes
* to accelerate, and therefore how long to decelerate, so
* we can work out what angle to start decelerating from */
if (decelFromAngle < 0) {
decelFromAngle = target-angle;
while (decelFromAngle < 0) decelFromAngle += radians;
while (decelFromAngle > radians) decelFromAngle -= radians;
}
}
} else {
if (!allowDecel && (angle < decelFromAngle) && (angle+speed >= decelFromAngle)) allowDecel = true;
if (allowDecel) speed = speed - accel;
if (speed < minSpeed) speed = minSpeed;
if (speed == minSpeed && angle < target && angle+speed >= target) return;
}
var r = i/2;
if (r > ballTrack) r = ballTrack;
var x = centreX+Math.cos(angle)*r;
var y = centreY+Math.sin(angle)*r;
g.setColor('#000000');
g.fillCircle(oldx,oldy,ballSize+1);
g.setColor('#ffffff');
g.fillCircle(x, y, ballSize);
oldx=x;
oldy=y;
g.flip();
}
}
// choose a winning segment and animate its selection
function choose() {
var chosen = Math.floor(Math.random()*N);
var minAngle = (chosen/N)*radians;
var maxAngle = minAngle + arclen;
animateChoice((minAngle+maxAngle)/2);
g.setColor(colours[chosen%colours.length]);
for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep)
arc(i, perimMax, minAngle, maxAngle);
arc(0, perimMax, minAngle, maxAngle);
for (var r = 1; r < segmentMax; r += circleStep)
g.fillCircle(centreX,centreY,r);
g.fillCircle(centreX,centreY,segmentMax);
}
// draw the current value of N in the middle of the screen, with
// up/down arrows
function drawN() {
g.setColor(g.theme.fg);
g.setFont("Vector",fontSize);
g.drawString(N,centreX-g.stringWidth(N)/2+4,centreY-fontSize/2);
if (N < maxN)
g.fillPoly([centreX-6,centreY-fontSize/2-7, centreX+6,centreY-fontSize/2-7, centreX, centreY-fontSize/2-14]);
if (N > minN)
g.fillPoly([centreX-6,centreY+fontSize/2+5, centreX+6,centreY+fontSize/2+5, centreX, centreY+fontSize/2+12]);
}
// update number of segments, with min/max limit, "arclen" update,
// and screen reset
function setN(n) {
N = n;
if (N < minN) N = minN;
if (N > maxN) N = maxN;
arclen = radians/N - gapAngle;
drawPerimeter();
}
// save N to choozi.txt
function writeN() {
var file = require("Storage").open("choozi.txt","w");
file.write(N);
}
// load N from choozi.txt
function readN() {
var file = require("Storage").open("choozi.txt","r");
var n = file.readLine();
if (n !== undefined) setN(parseInt(n));
else setN(defaultN);
}
shuffle(colours); // is this really best?
Bangle.setLCDTimeout(0); // keep screen on
readN();
drawN();
setWatch(() => {
writeN();
drawPerimeter();
choose();
}, BTN1, {repeat:true});
Bangle.on('touch', function(zone,e) {
if(e.x>+88){
setN(N-1);
drawN();
}else{
setN(N+1);
drawN();
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,7 +1,7 @@
{ {
"id": "choozi", "id": "choozi",
"name": "Choozi", "name": "Choozi",
"version": "0.03", "version": "0.04",
"description": "Choose people or things at random using Bangle.js.", "description": "Choose people or things at random using Bangle.js.",
"icon": "app.png", "icon": "app.png",
"tags": "tool", "tags": "tool",
@ -10,8 +10,10 @@
"allow_emulator": true, "allow_emulator": true,
"screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}], "screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}],
"storage": [ "storage": [
{"name":"choozi.app.js","url":"app.js","supports": ["BANGLEJS"]}, {"name":"choozi.app.js","url":"app.js"},
{"name":"choozi.app.js","url":"appb2.js","supports": ["BANGLEJS2"]},
{"name":"choozi.img","url":"app-icon.js","evaluate":true} {"name":"choozi.img","url":"app-icon.js","evaluate":true}
],
"data": [
{"name":"choozi.save"}
] ]
} }

View File

@ -32,3 +32,12 @@
Use widget_utils if available Use widget_utils if available
0.17: Load circles from clkinfo 0.17: Load circles from clkinfo
0.18: Improved clkinfo handling and using it for the weather circle 0.18: Improved clkinfo handling and using it for the weather circle
0.19: Remove old code and fixing clkinfo handling (fix HRM and other items that change)
Remove settings for what is displayed and instead allow circles to be changed by swiping
0.20: Add much faster circle rendering (250ms -> 40ms)
Add fast load capability
0.21: Remade all icons without a palette for dark theme
Now re-adds widgets if they were hidden when fast-loading
0.22: Fixed crash if item has no image and cutting long overflowing text
0.23: Setting circles colours per clkinfo and not position
0.24: Using suggested color from clock_info if set as default and available

View File

@ -5,6 +5,7 @@ A clock with three or four circles for different data at the bottom in a probabl
By default the time, date and day of week is shown. By default the time, date and day of week is shown.
It can show the following information (this can be configured): It can show the following information (this can be configured):
* Steps * Steps
* Steps distance * Steps distance
* Heart rate (automatically updates when screen is on and unlocked) * Heart rate (automatically updates when screen is on and unlocked)
@ -14,15 +15,24 @@ It can show the following information (this can be configured):
* Temperature inside circle * Temperature inside circle
* Condition as icon below circle * Condition as icon below circle
* Big weather icon next to clock * Big weather icon next to clock
* Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation)) * Altitude from internal pressure sensor
* Temperature, air pressure or altitude from internal pressure sensor * Active alarms (if `Alarm` app installed)
* Sunrise or sunset (if `Sunrise Clockinfo` app installed)
To change what is shown:
The color of each circle can be configured. The following colors are available: * Unlock the watch
* Tap on the circle to change (a border is drawn around it)
* Swipe up/down to change the guage within the given group
* Swipe left/right to change the group (eg. between standard Bangle.js and Alarms/etc)
Data is provided by ['Clock Info'](http://www.espruino.com/Bangle.js+Clock+Info)
so any apps that implement this feature can add extra information to be displayed.
The color of each circle can be configured from `Settings -> Apps -> Circles Clock`. The following colors are available:
* Basic colors (red, green, blue, yellow, magenta, cyan, black, white) * Basic colors (red, green, blue, yellow, magenta, cyan, black, white)
* Color depending on value (green -> red, red -> green) * Color depending on value (green -> red, red -> green)
## Screenshots ## Screenshots
![Screenshot dark theme](screenshot-dark.png) ![Screenshot dark theme](screenshot-dark.png)
![Screenshot light theme](screenshot-light.png) ![Screenshot light theme](screenshot-light.png)
@ -38,5 +48,5 @@ The color of each circle can be configured. The following colors are available:
Marco ([myxor](https://github.com/myxor)) Marco ([myxor](https://github.com/myxor))
## Icons ## Icons
Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from
[icons8](https://icons8.com/icon/set/weather/small--static--black) [icons8](https://icons8.com/icon/set/weather/small--static--black)

File diff suppressed because it is too large Load Diff

View File

@ -3,27 +3,18 @@
"showWidgets": false, "showWidgets": false,
"weatherCircleData": "humidity", "weatherCircleData": "humidity",
"circleCount": 3, "circleCount": 3,
"circle1": "Bangle/HRM", "Bangle/Battery_color":"red-green",
"circle2": "Bangle/Steps", "Bangle/Steps_color":"#0000ff",
"circle3": "Bangle/Battery", "Bangle/HRM_color":"green-red",
"circle4": "weather", "Bangle/Altitude_color":"#00ff00",
"circle1color": "green-red", "Weather/humidity_color":"#00ffff",
"circle2color": "#0000ff", "Weather/wind_color":"fg",
"circle3color": "red-green", "Weather/temperature_color":"blue-red",
"circle4color": "#ffff00", "Alarms_color":"#00ff00",
"circle1colorizeIcon": true, "circle1colorizeIcon": true,
"circle2colorizeIcon": true, "circle2colorizeIcon": true,
"circle3colorizeIcon": true, "circle3colorizeIcon": true,
"circle4colorizeIcon": false, "circle4colorizeIcon": false,
"updateInterval": 60, "updateInterval": 60,
"showBigWeather": false, "showBigWeather": false
"minHR": 40,
"maxHR": 200,
"confidence": 0,
"stepGoal": 10000,
"stepDistanceGoal": 8000,
"stepLength": 0.8,
"hrmValidity": 60
} }

View File

@ -1,7 +1,7 @@
{ "id": "circlesclock", { "id": "circlesclock",
"name": "Circles clock", "name": "Circles clock",
"shortName":"Circles clock", "shortName":"Circles clock",
"version":"0.18", "version":"0.24",
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style", "description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}], "screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],

View File

@ -12,29 +12,10 @@
storage.write(SETTINGS_FILE, settings); storage.write(SETTINGS_FILE, settings);
} }
//const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude", "timer"]; const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff",
//const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude", "timer"]; "#00ffff", "#fff", "#000", "green-red", "red-green", "blue-red", "red-blue", "fg"];
var valuesCircleTypes = ["empty","weather", "sunprogress"]; const namesColors = ["default", "red", "green", "blue", "yellow", "magenta",
var namesCircleTypes = ["empty","weather", "sun"]; "cyan", "white", "black", "green->red", "red->green", "blue->red", "red->blue", "foreground"];
clock_info.load().forEach(e=>{
if(e.dynamic) {
valuesCircleTypes = valuesCircleTypes.concat([e.name+"/"]);
namesCircleTypes = namesCircleTypes.concat([e.name]);
} else {
let values = e.items.map(i=>e.name+"/"+i.name);
let names =e.name=="Bangle" ? e.items.map(i=>i.name) : values;
valuesCircleTypes = valuesCircleTypes.concat(values);
namesCircleTypes = namesCircleTypes.concat(names);
}
})
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff",
"#00ffff", "#fff", "#000", "green-red", "red-green", "fg"];
const namesColors = ["default", "red", "green", "blue", "yellow", "magenta",
"cyan", "white", "black", "green->red", "red->green", "foreground"];
const weatherData = ["empty", "humidity", "wind"];
function showMainMenu() { function showMainMenu() {
let menu ={ let menu ={
@ -47,31 +28,11 @@
step: 1, step: 1,
onchange: x => save('circleCount', x), onchange: x => save('circleCount', x),
}, },
/*LANG*/'circle 1': ()=>showCircleMenu(1),
/*LANG*/'circle 2': ()=>showCircleMenu(2),
/*LANG*/'circle 3': ()=>showCircleMenu(3),
/*LANG*/'circle 4': ()=>showCircleMenu(4),
/*LANG*/'battery warn': {
value: settings.batteryWarn,
min: 10,
max : 100,
step: 10,
format: x => {
return x + '%';
},
onchange: x => save('batteryWarn', x),
},
/*LANG*/'show widgets': { /*LANG*/'show widgets': {
value: !!settings.showWidgets, value: !!settings.showWidgets,
format: () => (settings.showWidgets ? 'Yes' : 'No'), format: () => (settings.showWidgets ? 'Yes' : 'No'),
onchange: x => save('showWidgets', x), onchange: x => save('showWidgets', x),
}, },
/*LANG*/'weather data': {
value: weatherData.indexOf(settings.weatherCircleData),
min: 0, max: 2,
format: v => weatherData[v],
onchange: x => save('weatherCircleData', weatherData[x]),
},
/*LANG*/'update interval': { /*LANG*/'update interval': {
value: settings.updateInterval, value: settings.updateInterval,
min: 0, min: 0,
@ -82,47 +43,54 @@
}, },
onchange: x => save('updateInterval', x), onchange: x => save('updateInterval', x),
}, },
//TODO deprecated local icons, may disappear in future
/*LANG*/'legacy weather icons': {
value: !!settings.legacyWeatherIcons,
format: () => (settings.legacyWeatherIcons ? 'Yes' : 'No'),
onchange: x => save('legacyWeatherIcons', x),
},
/*LANG*/'show big weather': { /*LANG*/'show big weather': {
value: !!settings.showBigWeather, value: !!settings.showBigWeather,
format: () => (settings.showBigWeather ? 'Yes' : 'No'), format: () => (settings.showBigWeather ? 'Yes' : 'No'),
onchange: x => save('showBigWeather', x), onchange: x => save('showBigWeather', x),
} },
/*LANG*/'colorize icons': ()=>showCircleMenus()
}; };
clock_info.load().forEach(e=>{
if(e.dynamic) {
const colorKey = e.name + "_color";
menu[e.name+/*LANG*/' color'] = {
value: valuesColors.indexOf(settings[colorKey]) || 0,
min: 0, max: valuesColors.length - 1,
format: v => namesColors[v],
onchange: x => save(colorKey, valuesColors[x]),
};
} else {
let values = e.items.map(i=>e.name+"/"+i.name);
let names = e.name=="Bangle" ? e.items.map(i=>i.name) : values;
values.forEach((v,i)=>{
const colorKey = v + "_color";
menu[names[i]+/*LANG*/' color'] = {
value: valuesColors.indexOf(settings[colorKey]) || 0,
min: 0, max: valuesColors.length - 1,
format: v => namesColors[v],
onchange: x => save(colorKey, valuesColors[x]),
};
});
}
})
E.showMenu(menu); E.showMenu(menu);
} }
function showCircleMenu(circleId) { function showCircleMenus() {
const circleName = "circle" + circleId; const menu = {
const colorKey = circleName + "color"; '': { 'title': /*LANG*/'Colorize icons'},
const colorizeIconKey = circleName + "colorizeIcon"; /*LANG*/'< Back': ()=>showMainMenu(),
};
const menu = { for(var circleId=1; circleId<=4; ++circleId) {
'': { 'title': /*LANG*/'Circle ' + circleId }, const circleName = "circle" + circleId;
/*LANG*/'< Back': ()=>showMainMenu(), const colorKey = circleName + "color";
/*LANG*/'data': { const colorizeIconKey = circleName + "colorizeIcon";
value: valuesCircleTypes.indexOf(settings[circleName]), menu[/*LANG*/'circle ' + circleId] = {
min: 0, max: valuesCircleTypes.length - 1,
format: v => namesCircleTypes[v],
onchange: x => save(circleName, valuesCircleTypes[x]),
},
/*LANG*/'color': {
value: 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: settings[colorizeIconKey] || false, value: settings[colorizeIconKey] || false,
format: () => (settings[colorizeIconKey] ? 'Yes' : 'No'), format: () => (settings[colorizeIconKey]? /*LANG*/'Yes': /*LANG*/'No'),
onchange: x => save(colorizeIconKey, x), onchange: x => save(colorizeIconKey, x),
}, };
}; }
E.showMenu(menu); E.showMenu(menu);
} }

View File

@ -0,0 +1 @@
0.01: New App!

BIN
apps/clkinfocal/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

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