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/qrcode/qr-scanner.umd.min.js
apps/gipy/pkg/gpconv.js
apps/health/chart.min.js
*.test.js

View File

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

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ _site
.owncloudsync.log
Desktop.ini
.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,
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.
@ -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" : { "messages":"app" } // optional, depend on a specific app ID
// 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_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
// that contains more information about this app (usage, etc)
// A 'Read more...' link will be added under the app

View File

@ -1,140 +1,142 @@
class TwoK {
constructor() {
this.b = Array(4).fill().map(() => Array(4).fill(0));
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);
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;
const xo = yo/2;
h = g.getHeight()-yo;
w = g.getWidth()-yo;
bh = Math.floor(h/4);
bw = Math.floor(w/4);
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
g.setFont("Vector", 16).setColor(g.theme.fg).drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
for (y=0; y<4; ++y)
for (x=0; x<4; ++x) {
b = this.b[y][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);
if (b > 4) g.setColor(1, 1, 1);
else g.setColor(0, 0, 0);
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());
if (d==-1) { // shift x left
for (y=0; y<4; ++y) {
for (x=2; x>=0; x--)
if (this.b[y][x]==0) {
for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
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];
{ // wrap app in scope to prevent minifier from removing the class definition completely
class TwoK {
constructor() {
this.b = Array(4).fill().map(() => Array(4).fill(0));
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);
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;
const xo = yo/2;
h = g.getHeight()-yo;
w = g.getWidth()-yo;
bh = Math.floor(h/4);
bw = Math.floor(w/4);
g.clearRect(0, 0, g.getWidth()-1, yo).setFontAlign(0, 0, 0);
g.setFont("Vector", 16).setColor(g.theme.fg).drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
this.drawBRect(xo-3, yo-3, xo+w+2, yo+h+2, 4, "#a88", "#caa", false);
for (y=0; y<4; ++y)
for (x=0; x<4; ++x) {
b = this.b[y][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);
if (b > 4) g.setColor(1, 1, 1);
else g.setColor(0, 0, 0);
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());
if (d==-1) { // shift x left
for (y=0; y<4; ++y) {
for (x=2; x>=0; x--)
if (this.b[y][x]==0) {
for (i=x; i<3; i++) this.b[y][i] = this.b[y][i+1];
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
for (y=0; y<4; ++y) {
for (x=1; x<4; x++)
if (this.b[y][x]==0) {
for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
this.b[y][0] = 0;
}
for (x=3; x>0; --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>0; j--) this.b[y][j] = this.b[y][j-1];
this.b[y][0] = 0;
}
else if (d==1) { // shift x right
for (y=0; y<4; ++y) {
for (x=1; x<4; x++)
if (this.b[y][x]==0) {
for (i=x; i>0; i--) this.b[y][i] = this.b[y][i-1];
this.b[y][0] = 0;
}
for (x=3; x>0; --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>0; j--) this.b[y][j] = this.b[y][j-1];
this.b[y][0] = 0;
}
}
}
}
else if (d==-2) { // shift y down
for (x=0; x<4; ++x) {
for (y=1; y<4; y++)
if (this.b[y][x]==0) {
for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
this.b[0][x] = 0;
}
for (y=3; y>0; y--)
if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
this.score += 2*this.b[y][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];
this.b[0][x] = 0;
}
else if (d==-2) { // shift y down
for (x=0; x<4; ++x) {
for (y=1; y<4; y++)
if (this.b[y][x]==0) {
for (i=y; i>0; i--) this.b[i][x] = this.b[i-1][x];
this.b[0][x] = 0;
}
for (y=3; y>0; y--)
if (this.b[y][x]==this.b[y-1][x] || this.b[y][x]==0) {
this.score += 2*this.b[y][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];
this.b[0][x] = 0;
}
}
}
}
else if (d==2) { // shift y up
for (x=0; x<4; ++x) {
for (y=2; y>=0; y--)
if (this.b[y][x]==0) {
for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
this.b[3][x] = 0;
}
for (y=0; y<3; ++y)
if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
this.score += 2*this.b[y][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];
this.b[3][x] = 0;
}
else if (d==2) { // shift y up
for (x=0; x<4; ++x) {
for (y=2; y>=0; y--)
if (this.b[y][x]==0) {
for (i=y; i<3; i++) this.b[i][x] = this.b[i+1][x];
this.b[3][x] = 0;
}
for (y=0; y<3; ++y)
if (this.b[y][x]==this.b[y+1][x] || this.b[y][x]==0) {
this.score += 2*this.b[y][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];
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) {
if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
var res = false;
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);
function dragHandler(e) {
if (e.b && (Math.abs(e.dx)>7 || Math.abs(e.dy)>7)) {
var res = false;
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);
}
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();
twok.addDigit(); twok.addDigit();
twok.render();
if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
if (process.env.HWVERSION==1) {
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(); }, BTN3, {repeat: true});
}
var twok = new TwoK();
twok.addDigit(); twok.addDigit();
twok.render();
if (process.env.HWVERSION==2) Bangle.on("drag", dragHandler);
if (process.env.HWVERSION==1) {
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(); }, BTN3, {repeat: true});
}
}

View File

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

View File

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

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Fullscreen settings.
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 locale = require('locale');
const storage = require('Storage');
const widget_utils = require('widget_utils');
/*
@ -109,7 +110,7 @@ function draw() {
// Draw widgets if not fullscreen
if(settings.fullscreen){
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
widget_utils.hide();
} else {
Bangle.drawWidgets();
}

View File

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

View File

@ -1 +1,4 @@
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
* 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
* 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
* 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
[@alainsaas](https://github.com/alainsaas)

View File

@ -18,19 +18,29 @@ var timervalue = 0;
var istimeron = false;
var timertick;
Bangle.on('touch',t=>{
if (t == 1) {
Bangle.on('touch',(touchside, touchdata)=>{
if (touchside == 1) {
Bangle.buzz(30);
if (timervalue < 5*60) { timervalue = 1 ; }
else { timervalue -= 5*60; }
var changevalue = 0;
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);
if (!istimeron) {
istimeron = true;
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() {
g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6);
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);
setTimeout(function(){ g.reset().clearRect(0, 76, 44+44, g.getHeight()/2+6); }, 8000);
}
// time
var offsets = require("Storage").readJSON("a_clock_timer.settings.json") || [ ["PAR",1], ["TYO",9] ];
var drawTimeout;
function getGmt() {
@ -102,20 +113,34 @@ function queueNextDraw() {
function draw() {
g.reset().clearRect(0,24,g.getWidth(),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.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 date = new Date();
g.setFontAlign(0,0);
g.setFont("Michroma36").drawString(locale.time(date,1), g.getWidth()/2, 46);
g.setFont("6x8");
g.drawString(locale.date(new Date(),1), 125, 68);
g.drawString("PAR "+locale.time(getTimeFromTimezone(1),1), 125, 80);
g.drawString("TYO "+locale.time(getTimeFromTimezone(9),1), 125, 88);
g.drawString(offsets[0][0]+" "+locale.time(getTimeFromTimezone(offsets[0][1]),1), 125, 80);
g.drawString(offsets[1][0]+" "+locale.time(getTimeFromTimezone(offsets[1][1]),1), 125, 88);
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",
"name": "A Clock with Timer",
"version": "0.01",
"version": "1.00",
"description": "A Clock with Timer, Map and Time Zones",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot-1.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"custom": "custom.html",
"storage": [
{"name":"a_clock_timer.app.js","url":"app.js"},
{"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.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.setTheme({bg:"#fff",fg:"#000",dark:false}).clear();
require("FontHaxorNarrow7x17").add(Graphics);
g.clear();
Bangle.loadWidgets();

View File

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

View File

@ -7,3 +7,7 @@
0.07: Clkinfo improvements.
0.08: Fix error in clkinfo (didn't require Storage & locale)
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 getPassedSec(date) {
var now = new Date();
var passed = (now-date)/1000;
if(passed<0) return 0;
return passed;
}
var agendaItems = {
name: "Agenda",
img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="),
dynamic: true,
items: []
};
var locale = require("locale");
@ -15,12 +22,16 @@
var title = entry.title.slice(0,12);
var date = new Date(entry.timestamp*1000);
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) : "";
agendaItems.items.push({
name: "Agenda "+i,
get: () => ({ text: title + "\n" + dateStr, img: null}),
show: function() { agendaItems.items[i].emit("redraw"); },
hasRange: true,
get: () => ({ text: title + "\n" + dateStr,
img: agendaItems.img, short: shortStr.trim(),
v: getPassedSec(date), min: 0, max: entry.durationInSeconds}),
show: function() {},
hide: function () {}
});
});

View File

@ -33,16 +33,32 @@ CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
function getDate(timestamp) {
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) {
let shortTime = Locale.time(date,1)+Locale.meridian(date);
if(allDay) shortTime = "";
if(includeDay || allDay)
return Locale.date(date)+" "+shortTime;
if(includeDay || allDay) {
return formatDay(date)+" "+shortTime;
}
return shortTime;
}
function formatDateShort(date, allDay) {
return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay?
"" : Locale.time(date,1)+Locale.meridian(date));
return formatDay(date).replace(/\d\d\d\d/,"")+(allDay?"":Locale.time(date,1)+Locale.meridian(date));
}
var lines = [];

View File

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

View File

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

View File

@ -3,3 +3,6 @@
0.03: Do not load AGPS data on boot
Increase minimum interval to 6 hours
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;
function start() {
function start(restart) {
g.reset();
g.clear();
waiting = false;
display("Retry?", "touch to retry");
if (!restart) {
display("Start?", "touch to start");
}
else {
display("Retry?", "touch to retry");
}
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() {
@ -36,7 +50,7 @@ function updateAgps() {
g.clear();
if (!waiting) {
waiting = true;
display("Updating A-GPS...", "takes ~ 10 seconds");
display("Updating A-GPS...", "takes ~10 seconds");
require("agpsdata").pull(function() {
waiting = false;
display("A-GPS updated.", "touch to close");
@ -45,10 +59,10 @@ function updateAgps() {
function(error) {
waiting = false;
E.showAlert(error, "Error")
.then(() => { start(); });
.then(() => { start(true); });
});
} else {
display("Waiting...");
}
}
updateAgps();
start(false);

View File

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

View File

@ -2,7 +2,7 @@
"name": "A-GPS Data Downloader App",
"shortName":"A-GPS Data",
"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.",
"tags": "boot,tool,assisted,gps,agps,http",
"allow_emulator":true,

View File

@ -2,3 +2,6 @@
0.02: Design improvements and fixes.
0.03: Indicate battery level through line occurrence.
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
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)

View File

@ -1,6 +1,13 @@
/**
/************************************************
* AI Clock
*/
const clock_info = require("clock_info");
/************************************************
* Assets
*/
require("Font7x11Numeric7Seg").add(Graphics);
Graphics.prototype.setFontGochiHand = function(scale) {
// Actual height 27 (29 - 3)
@ -13,33 +20,14 @@ Graphics.prototype.setFontGochiHand = function(scale) {
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;
/*
* 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() {
function drawBackground(start, end) {
g.setFontAlign(0,0);
g.setColor(g.theme.fg);
g.setColor("#000");
var bat = E.getBattery() / 100.0;
var y = 0;
while(y < H){
var y = start;
while(y < end){
// Show less lines in case of small battery level.
if(Math.random() > bat){
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){
g.setColor(g.theme.fg);
g.fillCircle(cx, cy, 12);
@ -64,19 +76,12 @@ function drawCircle(isLocked){
g.fillCircle(cx, cy, 6);
}
function toAngle(a){
if (a < 0){
return 360 + a;
}
if(a > 360) {
return 360 - a;
}
return a
}
function drawTime(){
// Draw digital time first
drawDigits();
// And now the analog time
var drawHourHand = g.drawRotRect.bind(g,8,12,R-38);
var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 );
@ -90,13 +95,6 @@ function drawTime(){
h += date.getMinutes()/60.0;
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
g.setColor(g.theme.fg);
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(){
var date = new Date();
@ -156,19 +132,22 @@ function drawDigits(){
}
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
queueDraw();
g.setColor("#fff");
g.reset().clearRect(0, clkInfoY, g.getWidth(), g.getHeight());
g.reset();
g.clearRect(0, 0, g.getWidth(), g.getHeight());
g.setColor(1,1,1);
drawBackground();
drawDate();
drawDigits();
drawBackground(clkInfoY, H);
drawTime();
drawCircle(Bangle.isLocked());
}
@ -179,7 +158,7 @@ function draw(){
*/
Bangle.on('lcdPower',on=>{
if (on) {
draw(true);
draw();
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
@ -191,6 +170,12 @@ Bangle.on('lock', function(isLocked) {
});
E.on("kill", function(){
clockInfoMenu.remove();
delete clockInfoMenu;
});
/*
* 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.
@ -216,8 +249,9 @@ Bangle.loadWidgets();
* area to the top bar doesn't get cleared.
*/
require('widget_utils').hide();
// 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();
// 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",
"shortName":"AI Clock",
"icon": "aiclock.png",
"version":"0.04",
"version":"0.07",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.",
@ -11,7 +11,9 @@
"tags": "clock",
"screenshots": [
{"url":"orig.png"},
{"url":"impl.png"}
{"url":"impl.png"},
{"url":"impl_2.png"},
{"url":"impl_3.png"}
],
"storage": [
{"name":"aiclock.app.js","url":"aiclock.app.js"},

View File

@ -36,4 +36,5 @@
0.33: Allow hiding timers&alarms
0.34: Add "Confirm" option to alarm/timer edit menus
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
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.
## Menu overview
- `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)
- `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`
- `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

View File

@ -48,9 +48,10 @@ function showMainMenu() {
};
alarms.forEach((e, index) => {
var label = e.timer
var label = (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] = {
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
@ -67,11 +68,12 @@ function showNewMenu() {
"": { "title": /*LANG*/"New..." },
"< Back": () => showMainMenu(),
/*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 alarm = require("sched").newDefaultAlarm();
@ -82,11 +84,16 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
}
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 = {
"": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" },
"": { "title": title },
"< Back": () => {
prepareAlarmForSave(alarm, alarmIndex, time);
prepareAlarmForSave(alarm, alarmIndex, time, date);
saveAndReload();
showMainMenu();
},
@ -106,6 +113,36 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
wrap: true,
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": {
value: alarm.on,
onchange: v => alarm.on = v
@ -115,8 +152,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => {
alarm.rp = repeat;
alarm.dow = dow;
alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
})
},
/*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) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
@ -145,7 +191,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) {
showMainMenu();
} else {
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);
}
function prepareAlarmForSave(alarm, alarmIndex, time) {
function prepareAlarmForSave(alarm, alarmIndex, time, date, temp) {
alarm.t = require("time_utils").encodeTime(time);
alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0;
if(date) alarm.date = date.toLocalISOString().slice(0,10);
if (alarmIndex === undefined) {
alarms.push(alarm);
} else {
alarms[alarmIndex] = alarm;
if(!temp) {
if (alarmIndex === undefined) {
alarms.push(alarm);
} else {
alarms[alarmIndex] = alarm;
}
}
}
@ -255,6 +304,8 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
}
var time = require("time_utils").decodeTime(timer.timer);
var keyboard = "textinput";
try {keyboard = require(keyboard);} catch(e) {keyboard = null;}
const menu = {
"": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" },
@ -285,6 +336,18 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
wrap: true,
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": {
value: timer.on,
onchange: v => timer.on = v
@ -306,6 +369,7 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
}
};
if (!keyboard) delete menu[/*LANG*/"Message"];
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
@ -324,15 +388,17 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
E.showMenu(menu);
}
function prepareTimerForSave(timer, timerIndex, time) {
function prepareTimerForSave(timer, timerIndex, time, temp) {
timer.timer = require("time_utils").encodeTime(time);
timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer;
timer.last = 0;
if (timerIndex === undefined) {
alarms.push(timer);
} else {
alarms[timerIndex] = timer;
if (!temp) {
if (timerIndex === undefined) {
alarms.push(timer);
} else {
alarms[timerIndex] = timer;
}
}
}

View File

@ -2,17 +2,16 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.35",
"version": "0.37",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm,widget",
"tags": "tool,alarm",
"supports": [ "BANGLEJS", "BANGLEJS2" ],
"readme": "README.md",
"dependencies": { "scheduler":"type" },
"dependencies": { "scheduler":"type", "alarm":"widget" },
"storage": [
{ "name": "alarm.app.js", "url": "app.js" },
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true },
{ "name": "alarm.wid.js", "url": "widget.js" }
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true }
],
"screenshots": [
{ "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.17: Now kick off Calendar sync as soon as connected to Gadgetbridge
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",
id:"call", src:"Phone",
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);
},
"alarm" : function() {
@ -134,7 +134,11 @@
event.satellites = NaN;
event.course = NaN;
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() {
gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 });
@ -148,7 +152,7 @@
Bangle.http = (url,options)=>{
options = options||{};
if (!NRF.getSecurityStatus().connected)
return Promise.reject("Not connected to Bluetooth");
return Promise.reject(/*LANG*/"Not connected to Bluetooth");
if (Bangle.httpRequest === undefined)
Bangle.httpRequest={};
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)
Bangle.setGPSPower = (isOn, appID) => {
// 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
if (!Bangle._PWR) Bangle._PWR={};
if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[];

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"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.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",

View File

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

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Update to work with 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) {
var bin = atob(b64);
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
//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
@ -158,6 +158,7 @@
var chunk = bin.substr(i,chunkSize);
js += `\x10Serial1.write(atob("${btoa(chunk)}"))\n`;
}
js = "\x10setTimeout(() => Bangle.setGPSPower(0,'agps'), 1000);\n"; // turn GPS off after a delay
return js;
}

View File

@ -1,7 +1,7 @@
{
"id": "assistedgps",
"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.",
"sortorder": -1,
"icon": "app.png",

View File

@ -1,2 +1,4 @@
0.01: Create astrocalc app
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
*/
const SunCalc = require("suncalc.js");
const SunCalc = require("suncalc"); // from modules folder
const storage = require("Storage");
const LAST_GPS_FILE = "astrocalc.gps.json";
let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null);
const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
function drawMoon(phase, x, y) {
const moonImgFiles = [
@ -73,7 +72,7 @@ function drawTitle(key) {
*/
function drawPoint(angle, radius, color) {
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 a = angle * pRad;
@ -141,6 +140,7 @@ function drawData(title, obj, startX, startY) {
function drawMoonPositionPage(gps, title) {
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 = {
Azimuth: pos.azimuth.toFixed(2),
@ -150,59 +150,61 @@ function drawMoonPositionPage(gps, title) {
};
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();
drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1});
drawPoint(azimuthDegrees, 8, moonColor);
let m = setWatch(() => {
let m = moonIndexPageMenu(gps);
}, BTN3, {repeat: false, edge: "falling"});
}, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
}
function drawMoonIlluminationPage(gps, title) {
const phaseNames = [
"New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous",
"Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent",
/*LANG*/"New Moon", /*LANG*/"Waxing Crescent", /*LANG*/"First Quarter", /*LANG*/"Waxing Gibbous",
/*LANG*/"Full Moon", /*LANG*/"Waning Gibbous", /*LANG*/"Last Quater", /*LANG*/"Waning Crescent",
];
const phase = SunCalc.getMoonIllumination(new Date());
const phaseIdx = Math.round(phase.phase*8);
const pageData = {
Phase: phaseNames[phase.phase],
Phase: phaseNames[phaseIdx],
};
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 = moonIndexPageMenu(gps);
}, BTN3, {repease: false, edge: "falling"});
}, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
}
function drawMoonTimesPage(gps, title) {
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 = {
Rise: dateToTimeString(times.rise),
Set: dateToTimeString(times.set),
};
drawData(title, pageData, null, 105);
drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
drawPoints();
// Draw the moon rise position
const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI);
drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1});
drawPoint(riseAzimuthDegrees, 8, moonColor);
// Draw the moon set position
const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1});
drawPoint(setAzimuthDegrees, 8, moonColor);
let m = setWatch(() => {
let m = moonIndexPageMenu(gps);
}, BTN3, {repease: false, edge: "falling"});
}, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
}
function drawSunShowPage(gps, key, date) {
@ -224,7 +226,7 @@ function drawSunShowPage(gps, key, date) {
Degrees: azimuthDegrees
};
drawData(key, pageData, null, 85);
drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
drawPoints();
@ -233,7 +235,7 @@ function drawSunShowPage(gps, key, date) {
m = setWatch(() => {
m = sunIndexPageMenu(gps);
}, BTN3, {repeat: false, edge: "falling"});
}, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
return null;
}
@ -273,15 +275,15 @@ function moonIndexPageMenu(gps) {
},
"Times": () => {
m = E.showMenu();
drawMoonTimesPage(gps, "Times");
drawMoonTimesPage(gps, /*LANG*/"Times");
},
"Position": () => {
m = E.showMenu();
drawMoonPositionPage(gps, "Position");
drawMoonPositionPage(gps, /*LANG*/"Position");
},
"Illumination": () => {
m = E.showMenu();
drawMoonIlluminationPage(gps, "Illumination");
drawMoonIlluminationPage(gps, /*LANG*/"Illumination");
},
"< Back": () => m = indexPageMenu(gps),
};
@ -292,15 +294,15 @@ function moonIndexPageMenu(gps) {
function indexPageMenu(gps) {
const menu = {
"": {
"title": "Select",
"title": /*LANG*/"Select",
},
"Sun": () => {
/*LANG*/"Sun": () => {
m = sunIndexPageMenu(gps);
},
"Moon": () => {
/*LANG*/"Moon": () => {
m = moonIndexPageMenu(gps);
},
"< Exit": () => { load(); }
"< Back": () => { load(); }
};
return E.showMenu(menu);
@ -310,79 +312,10 @@ function getCenterStringX(str) {
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() {
Bangle.setGPSPower(1);
drawGPSWaitPage();
let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
indexPageMenu(location);
}
let m;
init();
init();

View File

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

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.15: Add Power saving option
0.16: Support Fast Loading
0.17: Hide widgets instead of not loading them at all

View File

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

View File

@ -1,5 +1,10 @@
(function(back) {
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) {
s[key] = value;
@ -19,7 +24,7 @@
};
let items = {
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)
if (process.env.HWVERSION>1) {

View File

@ -4,3 +4,5 @@
0.05: Update *on* the minute rather than every 15 secs
Now show widgets
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
// https://github.com/eska-muc/BangleApps
var settings = require('Storage').readJSON("berlinc.json", true) || {};
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;
var show_time = false;
var yy = 0;
let fullscreen = !!settings.fullscreen;
var rowlights = [];
var time_digit = [];
let show_date = false;
let show_time = false;
let yy = 0;
let rowlights = [];
let time_digit = [];
// timeout used to update every minute
var drawTimeout;
let drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
let queueDraw = () => {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
};
function draw() {
g.reset().clearRect(0,24,g.getWidth(),g.getHeight());
let draw = () => {
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();
// show date below the clock
@ -37,7 +46,7 @@ function draw() {
var dateString = `${yr}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`;
var strWidth = g.stringWidth(dateString);
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);
@ -50,15 +59,16 @@ function draw() {
time_digit[2] = Math.floor(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++) {
nfields = fields[row];
boxWidth = width / nfields;
boxWidth = (width - 1) / nfields;
for (col = 0; col < nfields; col++) {
x1 = col * boxWidth + offset;
x1 = col * boxWidth + x;
y1 = row * rowHeight + offset;
x2 = (col + 1) * boxWidth + offset;
x2 = (col + 1) * boxWidth + x;
y2 = (row + 1) * rowHeight + offset;
g.setColor(g.theme.fg).drawRect(x1, y1, x2, y2);
@ -84,33 +94,53 @@ function draw() {
queueDraw();
}
function toggleDate() {
let toggleDate = () => {
show_date = ! show_date;
draw();
}
function toggleTime() {
let toggleTime = () => {
show_time = ! show_time;
draw();
}
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
let clear = () => {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
let onLcdPower = on => {
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
clear();
}
});
}
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
Bangle.setUI("clockupdown", dir=> {
Bangle.setUI({mode: "clockupdown", remove: cleanup}, dir=> {
if (dir<0) toggleTime();
if (dir>0) toggleDate();
});
g.clear();
Bangle.loadWidgets();
if (fullscreen){
if (process.env.HWVERSION == 2) require("widget_utils").swipeOn();
else require("widget_utils").hide();
}
Bangle.drawWidgets();
draw();
}

View File

@ -1,7 +1,7 @@
{
"id": "berlinc",
"name": "Berlin Clock",
"version": "0.05",
"version": "0.07",
"description": "Berlin Clock (see https://en.wikipedia.org/wiki/Mengenlehreuhr)",
"icon": "berlin-clock.png",
"type": "clock",
@ -12,6 +12,8 @@
"screenshots": [{"url":"berlin-clock-screenshot.png"}],
"storage": [
{"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}
]
],
"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.53: Add polyfills for pre-2v15.135 firmware for Bangle.load and Bangle.showClock
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
recalculates, but this avoids us doing a whole bunch of reconfiguration most
of the time. */
{ // execute in our own scope so we don't have to free variables...
E.showMessage(/*LANG*/"Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
var FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1"));
var boot = "", bootPost = "";
let s = require('Storage').readJSON('setting.json',1)||{};
const BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
const FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1"));
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
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})`;
} 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 += ` { 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`;
delete Bangle.load;
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
// Append *.boot.js files
// These could change bleServices/bleServiceOptions if needed
var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
var getPriority = /.*\.(\d+)\.boot\.js$/;
var aPriority = a.match(getPriority);
var bPriority = b.match(getPriority);
let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{
let getPriority = /.*\.(\d+)\.boot\.js$/;
let aPriority = a.match(getPriority);
let bPriority = b.match(getPriority);
if (aPriority && bPriority){
return parseInt(aPriority[1]) - parseInt(bPriority[1]);
} 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);
});
// precalculate file size
var fileSize = boot.length + bootPost.length;
let fileSize = boot.length + bootPost.length;
bootFiles.forEach(bootFile=>{
// 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)
require('Storage').write('.boot0',boot,0,fileSize);
var fileOffset = boot.length;
let fileOffset = boot.length;
bootFiles.forEach(bootFile=>{
// we add a semicolon so if the file is wrapped in (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";
// but we need to do this without ever loading everything into RAM as some
// boot files seem to be getting pretty big now.
require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset);
fileOffset+=2+bootFile.length+1;
var bf = require('Storage').read(bootFile);
if (DEBUG) {
require('Storage').write('.boot0',`//${bootFile}\n`,fileOffset);
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
// Espruino wants to read the whole file into RAM first, and on Bangle.js 1
// it can be too big (especially BTHRM).
var bflen = bf.length;
var bfoffset = 0;
let bflen = bf.length;
let bfoffset = 0;
while (bflen) {
var bfchunk = Math.min(bflen, 2048);
let bfchunk = Math.min(bflen, 2048);
require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset);
fileOffset+=bfchunk;
bfoffset+=bfchunk;
@ -139,15 +160,14 @@ bootFiles.forEach(bootFile=>{
}
require('Storage').write('.boot0',";\n",fileOffset);
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);
delete boot;
delete bootPost;
delete bootFiles;
delete fileSize;
delete fileOffset;
E.showMessage(/*LANG*/"Reloading...");
eval(require('Storage').read('.boot0'));
}
// .bootcde should be run automatically after if required, since
// we normally get called automatically from '.boot0'
eval(require('Storage').read('.boot0'));

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.54",
"version": "0.55",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"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
Additional logging on errors
Add debug option for disabling active scanning
0.17: New GUI based on layout library

View File

@ -1,5 +1,5 @@
var intervalInt;
var intervalBt;
const BPM_FONT_SIZE="19%";
const VALUE_TIMEOUT=3000;
var BODY_LOCS = {
0: 'Other',
@ -7,46 +7,119 @@ var BODY_LOCS = {
2: 'Wrist',
3: 'Finger',
4: 'Hand',
5: 'Ear Lobe',
5: 'Earlobe',
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){
g.reset();
g.clearRect(0,y,g.getWidth(),y+75);
}
function draw(y, type, event) {
clear(y);
var px = g.getWidth()/2;
var str = event.bpm + "";
g.reset();
g.setFontAlign(0,0);
g.setFontVector(40).drawString(str,px,y+20);
str = "Event: " + type;
if (type === "HRM") {
str += " Confidence: " + event.confidence;
g.setFontVector(12).drawString(str,px,y+40);
str = " Source: " + (event.src ? event.src : "internal");
g.setFontVector(12).drawString(str,px,y+50);
function getRow(id, text, additionalInfo){
let additional = [];
let l = {
type:"h", c: [
{
type:"v",
width: g.getWidth()*0.4,
c: [
{type:"txt", halign:1, font:"8%", label:text, id:id+"text" },
{type:"txt", halign:1, font:BPM_FONT_SIZE, label:"--", id:id, bgCol: g.theme.bg }
]
},{
type:undefined, fillx:1
},{
type:"v",
valign: -1,
width: g.getWidth()*0.45,
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 : "");
g.setFontVector(12).drawString(str,px,y+40);
str= "";
if (event.location) str += "Loc: " + BODY_LOCS[event.location];
if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(",");
g.setFontVector(12).drawString(str,px,y+50);
str= "";
if (event.contact) str += " Contact: " + event.contact;
if (event.energy) str += " kJoule: " + event.energy.toFixed(0);
g.setFontVector(12).drawString(str,px,y+60);
return l;
}
var layout = new Layout( {
type:"v", c: [
getRow("int", "INT", ["Confidence"]),
getRow("agg", "HRM", ["Confidence", "Source"]),
getRow("bt", "BT", ["Battery","Location","Contact", "RR", "Energy"]),
{ type:undefined, height:8 } //dummy to protect debug output
]
}, {
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
function showStatusInfo(txt) {
@ -57,41 +130,26 @@ function showStatusInfo(txt) {
}
function onBtHrm(e) {
if (firstEventBt){
clear(24);
firstEventBt = false;
}
draw(100, "BTHRM", e);
if (e.bpm === 0){
Bangle.buzz(100,0.2);
}
if (intervalBt){
clearInterval(intervalBt);
}
intervalBt = setInterval(()=>{
clear(100);
}, 2000);
bt = e;
bt.time = Date.now();
}
function onHrm(e) {
if (firstEventInt){
clear(24);
firstEventInt = false;
}
draw(24, "HRM", e);
if (intervalInt){
clearInterval(intervalInt);
}
intervalInt = setInterval(()=>{
clear(24);
}, 2000);
function onInt(e) {
int = e;
int.time = Date.now();
}
function onAgg(e) {
agg = e;
agg.time = Date.now();
}
var settings = require('Storage').readJSON("bthrm.json", true) || {};
Bangle.on('BTHRM', onBtHrm);
Bangle.on('HRM', onHrm);
Bangle.on('HRM_int', onInt);
Bangle.on('HRM', onAgg);
Bangle.setHRMPower(1,'bthrm');
if (!(settings.startWithHrm)){
@ -103,10 +161,11 @@ Bangle.loadWidgets();
Bangle.drawWidgets();
if (Bangle.setBTHRMPower){
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 {
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'));

View File

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

View File

@ -2,9 +2,10 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.16",
"version": "0.17",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"screenshots": [{"url":"screen.png"}],
"type": "app",
"tags": "health,bluetooth,hrm,bthrm",
"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.21: On the default menu the week of year can be shown.
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
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
and additionally allows you to send triggers directly from the clock (select triggers via up/down and
send via click center). Here are examples of other apps that are integrated:
For example, if you install the HomeAssistant app, this menu item will be shown if you first
touch the bottom of the screen and then swipe left/right to the home assistant menu. To select
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.
- 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*
![](screenshot_3.png)
Note: If some apps are not installed (e.gt. weather app), then this menu item is hidden.
## Settings
- 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.
- 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 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>

View File

@ -1,10 +1,12 @@
{ // must be inside our own scope here so that when we are unloaded everything disappears
/************************************************
* Includes
*/
const locale = require('locale');
const storage = require('Storage');
const clock_info = require("clock_info");
const widget_utils = require("widget_utils");
/************************************************
* Globals
@ -12,8 +14,6 @@ const clock_info = require("clock_info");
const SETTINGS_FILE = "bwclk.setting.json";
const W = g.getWidth();
const H = g.getHeight();
var lock_input = false;
/************************************************
* Settings
@ -28,7 +28,20 @@ let settings = {
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || 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;
};
function imgLock(){
let imgLock = function() {
return {
width : 16, height : 16, bpp : 1,
transparent : 0,
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.
var bwItems = {
name: null,
img: null,
items: [
{ name: "WeekOfYear",
get: () => ({ text: "Week " + weekOfYear(), img: null}),
show: function() { bwItems.items[0].emit("redraw"); },
hide: function () {}
},
]
};
let clockInfoItems = clock_info.load();
function weekOfYear() {
// Add some custom clock-infos
let weekOfYear = function() {
var date = new Date();
date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
@ -111,87 +114,98 @@ function weekOfYear() {
- 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
var menu = clock_info.load();
menu = menu.concat(bwItems);
// Empty for large time
clockInfoItems[0].items.unshift({ name : "nop",
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
menu.forEach((menuItm, x) => {
menuItm.items.forEach((item, y) => {
function drawItem() {
// For the clock, we have a special case, as we don't wanna redraw
// immediately when something changes. Instead, we update data each minute
// to save some battery etc. Therefore, we hide (and disable the listener)
// immedeately after redraw...
item.hide();
let clockInfoMenu = clock_info.addInteractive(clockInfoItems, {
x : 0,
y: 135,
w: W,
h: H-135,
draw : (itm, info, options) => {
var hideClkInfo = info.text == null;
// After drawing the item, we enable inputs again...
lock_input = false;
g.setColor(g.theme.fg);
g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h);
var info = item.get();
drawMenuItem(info.text, info.img);
g.setFontAlign(0,0);
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
*/
function draw() {
let draw = function() {
// Queue draw again
queueDraw();
// Draw clock
drawDate();
drawMenuAndTime();
drawTime();
drawLock();
drawWidgets();
}
};
function drawDate(){
let drawDate = function() {
// Draw background
var y = H/5*2 + (isFullscreen() ? 0 : 8);
var y = getLineY()
g.reset().clearRect(0,0,W,y);
// Draw date
@ -216,17 +230,17 @@ function drawDate(){
g.setMediumFont();
g.setColor(g.theme.fg);
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
var y1 = getLineY();
var y = y1;
var date = new Date();
// Draw time
g.setColor(g.theme.bg);
g.setFontAlign(0,0);
var hours = String(date.getHours());
var minutes = date.getMinutes();
minutes = minutes < 10 ? String("0") + minutes : minutes;
@ -236,212 +250,93 @@ function drawTime(y, smallText){
// Set y coordinates correctly
y += parseInt((H - y)/2) + 5;
// Show large or small time depending on info entry
if(smallText){
if (hideClkInfo){
g.setLargeFont();
} else {
y -= 15;
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.fillRect(0,y,W,H);
g.fillRect(0,y1,W,y+20 + (hideClkInfo ? 1 : 0) + (isFullscreen() ? 3 : 0));
// Draw menu text
var hasText = (text != null && text != "");
if(hasText){
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);
}
g.setColor(g.theme.bg);
g.setFontAlign(0,0);
g.drawString(timeStr, W/2, y);
};
function drawMenuAndTime(){
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(){
let drawLock = function() {
if(settings.showLock && Bangle.isLocked()){
g.setColor(g.theme.fg);
g.drawImage(imgLock(), W-16, 2);
}
}
};
function drawWidgets(){
let drawWidgets = function() {
if(isFullscreen()){
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
widget_utils.hide();
} else {
Bangle.drawWidgets();
}
}
function isFullscreen(){
var s = settings.screen.toLowerCase();
if(s == "dynamic"){
return Bangle.isLocked()
} else {
return s == "full"
}
}
};
/************************************************
* Listener
*/
// timeout used to update every minute
var drawTimeout;
let drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
let queueDraw = function() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
};
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
let lcdListenerBw = function(on) {
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
};
Bangle.on('lcdPower', lcdListenerBw);
Bangle.on('lock', function(isLocked) {
let lockListenerBw = function(isLocked) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
if(!isLocked && settings.screen.toLowerCase() == "dynamic"){
// If we have to show the widgets again, we load it from our
// 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();
});
Bangle.on('charging',function(charging) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
};
Bangle.on('lock', lockListenerBw);
let charging = function(charging){
// Jump to battery
settings.menuPosX = 0;
settings.menuPosY = 1;
draw();
});
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...
}
});
clockInfoMenu.setItem(0, 2);
drawTime();
}
Bangle.on('charging', charging);
let kill = function(){
clockInfoMenu.remove();
delete clockInfoMenu;
};
E.on("kill", kill);
/************************************************
* 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
// is enabled. In order to draw the widgets correctly, we invert the
// 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();
// 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
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();
} // End of app scope

View File

@ -1,11 +1,11 @@
{
"id": "bwclk",
"name": "BW Clock",
"version": "0.23",
"description": "A very minimalistic clock to mainly show date and time.",
"version": "0.30",
"description": "A very minimalistic clock.",
"readme": "README.md",
"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",
"tags": "clock,clkinfo",
"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.04: Improve current time readability in light theme.
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;
if (lines.length > 2) {
lines = lines.slice(0,2);
lines[1] = lines[1].slice(0,-3)+"...";
lines[1] += "...";
}
g.drawString(lines.join('\n'),10,y);
y+=20 * lines.length;
if(event.location) {
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;
}
if (event.color) {
@ -131,4 +133,3 @@ var minuteInterval = setInterval(redraw, 60 * 1000);
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

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

View File

@ -1,3 +1,9 @@
0.01: New App!
0.02: Support Bangle.js 2
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
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
segments on the Bangle.js display.
## Controls
## Controls Bangle 1
BTN1: increase the number of segments
BTN2: choose a segment at random
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
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
*/
var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff'];
const GU = require("graphics_utils");
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 gapAngle = 0.035; // radians - gap between segments
var perimMin = 110; // px - min. radius of perimeter
var perimMax = 120; // px - max. radius of perimeter
var perimMin = g.getWidth()*0.40; // px - min. 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 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 accel = 0.0002; // rad/sec/sec - acc-/deceleration rate
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 centreY = 120; // px - centre of screen
var centreX = g.getWidth()*0.5; // px - centre of screen
var centreY = g.getWidth()*0.5; // px - centre of screen
var fontSize = 50; // px
@ -33,7 +34,6 @@ 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;
@ -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
function drawPerimeter() {
g.setBgColor('#000000');
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);
GU.fillArc(g, centreX, centreY, perimMin,perimMax,minAngle,minAngle+arclen, stepAngle);
}
}
@ -131,6 +103,7 @@ function animateChoice(target) {
g.fillCircle(x, y, ballSize);
oldx=x;
oldy=y;
if (process.env.HWVERSION == 2) g.flip();
}
}
@ -141,11 +114,15 @@ function choose() {
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)
for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep){
GU.fillArc(g, centreX, centreY, i, perimMax, minAngle, maxAngle, stepAngle);
if (process.env.HWVERSION == 2) g.flip();
}
GU.fillArc(g, centreX, centreY, 0, perimMax, minAngle, maxAngle, stepAngle);
for (var r = 1; r < segmentMax; r += circleStep){
g.fillCircle(centreX,centreY,r);
if (process.env.HWVERSION == 2) g.flip();
}
g.fillCircle(centreX,centreY,segmentMax);
}
@ -171,38 +148,47 @@ function setN(n) {
drawPerimeter();
}
// save N to choozi.txt
// save N to choozi.save
function writeN() {
var file = require("Storage").open("choozi.txt","w");
file.write(N);
var savedN = read();
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() {
var file = require("Storage").open("choozi.txt","r");
var n = file.readLine();
if (n !== undefined) setN(parseInt(n));
else setN(defaultN);
setN(read());
}
shuffle(colours); // is this really best?
Bangle.setLCDMode("direct");
Bangle.setLCDTimeout(0); // keep screen on
if (process.env.HWVERSION == 1){
colours=colours.concat(colours2);
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();
drawN();
setWatch(() => {
setN(N+1);
drawN();
}, BTN1, {repeat:true});
setWatch(() => {
writeN();
drawPerimeter();
choose();
}, BTN2, {repeat:true});
setWatch(() => {
setN(N-1);
drawN();
}, BTN3, {repeat:true});
Bangle.setUI("updown", (v)=>{
if (!v){
writeN();
drawPerimeter();
choose();
} else {
setN(N-v);
drawN();
}
});

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

View File

@ -32,3 +32,12 @@
Use widget_utils if available
0.17: Load circles from clkinfo
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.
It can show the following information (this can be configured):
* Steps
* Steps distance
* 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
* Condition as icon below circle
* Big weather icon next to clock
* Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation))
* Temperature, air pressure or altitude from internal pressure sensor
* 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)
* Color depending on value (green -> red, red -> green)
## Screenshots
![Screenshot dark theme](screenshot-dark.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))
## 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)

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,7 +1,7 @@
{ "id": "circlesclock",
"name": "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",
"icon": "app.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);
}
//const valuesCircleTypes = ["empty", "steps", "stepsDist", "hr", "battery", "weather", "sunprogress", "temperature", "pressure", "altitude", "timer"];
//const namesCircleTypes = ["empty", "steps", "distance", "heart", "battery", "weather", "sun", "temperature", "pressure", "altitude", "timer"];
var valuesCircleTypes = ["empty","weather", "sunprogress"];
var namesCircleTypes = ["empty","weather", "sun"];
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"];
const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff",
"#00ffff", "#fff", "#000", "green-red", "red-green", "blue-red", "red-blue", "fg"];
const namesColors = ["default", "red", "green", "blue", "yellow", "magenta",
"cyan", "white", "black", "green->red", "red->green", "blue->red", "red->blue", "foreground"];
function showMainMenu() {
let menu ={
@ -47,31 +28,11 @@
step: 1,
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': {
value: !!settings.showWidgets,
format: () => (settings.showWidgets ? 'Yes' : 'No'),
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': {
value: settings.updateInterval,
min: 0,
@ -82,47 +43,54 @@
},
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': {
value: !!settings.showBigWeather,
format: () => (settings.showBigWeather ? 'Yes' : 'No'),
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);
}
function showCircleMenu(circleId) {
const circleName = "circle" + circleId;
const colorKey = circleName + "color";
const colorizeIconKey = circleName + "colorizeIcon";
const menu = {
'': { 'title': /*LANG*/'Circle ' + circleId },
/*LANG*/'< Back': ()=>showMainMenu(),
/*LANG*/'data': {
value: valuesCircleTypes.indexOf(settings[circleName]),
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': {
function showCircleMenus() {
const menu = {
'': { 'title': /*LANG*/'Colorize icons'},
/*LANG*/'< Back': ()=>showMainMenu(),
};
for(var circleId=1; circleId<=4; ++circleId) {
const circleName = "circle" + circleId;
const colorKey = circleName + "color";
const colorizeIconKey = circleName + "colorizeIcon";
menu[/*LANG*/'circle ' + circleId] = {
value: settings[colorizeIconKey] || false,
format: () => (settings[colorizeIconKey] ? 'Yes' : 'No'),
onchange: x => save(colorizeIconKey, x),
},
};
format: () => (settings[colorizeIconKey]? /*LANG*/'Yes': /*LANG*/'No'),
onchange: x => save(colorizeIconKey, x),
};
}
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