Merge branch 'espruino:master' into tapkb2

pull/1741/head
frigis1 2022-04-24 00:47:06 -07:00 committed by GitHub
commit 2c15afece1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
160 changed files with 2902 additions and 476 deletions

1
.gitignore vendored
View File

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

View File

@ -17,7 +17,7 @@ class TwoK {
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("#fff").drawString("Score:"+this.score.toString(), g.getWidth()/2, 8);
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) {

2
apps/2047pp/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New app!
0.02: Better support for watch themes

View File

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

View File

@ -29,7 +29,7 @@ function showMenu() {
}
function viewLog(n) {
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
var f = require("Storage").open(getFileName(n), "r");
var records = 0, l = "", ll="";
while ((l=f.readLine())!==undefined) {records++;ll=l;}

View File

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

View File

@ -0,0 +1,13 @@
# Activity reminder
A reminder to take short walks for the ones with a sedentary lifestyle.
The alert will popup only if you didn't take your short walk yet
Differents settings can be personnalized:
- Enable : Enable/Disable the app
- Start hour: Hour to start the reminder
- End hour: Hour to end the reminder
- Max innactivity: Maximum innactivity time to allow before the alert. From 15 min to 60 min
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 15 min
- Min steps: Minimal amount of steps to count as an activity

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwYda7dtwAQNmwRB2wQMgO2CIXACJcNCIfYCJYOCCgQRNJQYRM2ADBgwR/CKprRWAKPQWZ0DCIjXLjYREGpYODAQVgCBB3Btj+EAoQAGO4IdCgImDCAwLCAoo4IF4J3DCIPDCIQ4FO4VtwARCAoIRGRgQCBa4IRCKAQRERgOwIIIRDAoOACIoIBwwRHLIqMCFgIRCGQQRIWAYRLYQoREWwTmHO4IRCFgLXHPoi/CbogAFEAIRCWwTpKEwZBCHwK5BCJZEBCJZcCGQTLDCJK/BAQIRKMoaSDOIYAFeQYRMcYRWBXIUAWYPACIq8DagfACJQLCCIYsBU4QRF7B9CAogRGI4QLCAoprIMoZKER5C/DAoShMAo4AGfAQFIACQ="))

View File

@ -0,0 +1,37 @@
function drawAlert(){
E.showPrompt("Inactivity detected",{
title:"Activity reminder",
buttons : {"Ok": true,"Dismiss": false}
}).then(function(v) {
if(v == true){
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - 3);
require("activityreminder").saveStepsArray(stepsArray);
}
if(v == false){
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - activityreminder.dismissDelayMin);
require("activityreminder").saveStepsArray(stepsArray);
}
load();
});
Bangle.buzz(400);
setTimeout(load, 10000);
}
function run(){
if(stepsArray.length == activityreminder.maxInnactivityMin){
if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){
drawAlert();
}
}else{
eval(require("Storage").read("activityreminder.settings.js"))(()=>load());
}
}
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
activityreminder = require("activityreminder").loadSettings();
stepsArray = require("activityreminder").loadStepsArray();
run();

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,29 @@
function run(){
var now = new Date();
var h = now.getHours();
if(h >= activityreminder.startHour && h < activityreminder.endHour){
var health = Bangle.getHealthStatus("day");
stepsArray.unshift(health.steps);
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin);
require("activityreminder").saveStepsArray(stepsArray);
}
else{
if(stepsArray != []){
stepsArray = [];
require("activityreminder").saveStepsArray(stepsArray);
}
}
if(stepsArray.length >= activityreminder.maxInnactivityMin){
if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){
load('activityreminder.app.js');
}
}
}
activityreminder = require("activityreminder").loadSettings();
if(activityreminder.enabled) {
stepsArray = require("activityreminder").loadStepsArray();
setInterval(run, 60000);
}

View File

@ -0,0 +1,22 @@
exports.loadSettings = function() {
return Object.assign({
enabled: true,
startHour: 9,
endHour: 20,
maxInnactivityMin: 30,
dismissDelayMin: 15,
minSteps: 50
}, require("Storage").readJSON("ar.settings.json", true) || {});
};
exports.writeSettings = function(settings){
require("Storage").writeJSON("ar.settings.json", settings);
};
exports.saveStepsArray = function(stepsArray) {
require("Storage").writeJSON("ar.stepsarray.json", stepsArray);
};
exports.loadStepsArray = function(){
return require("Storage").readJSON("ar.stepsarray.json") || [];
};

View File

@ -0,0 +1,22 @@
{
"id": "activityreminder",
"name": "Activity Reminder",
"shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
"version":"0.01",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name": "activityreminder.app.js", "url":"app.js"},
{"name": "activityreminder.boot.js", "url": "boot.js"},
{"name": "activityreminder.settings.js", "url": "settings.js"},
{"name": "activityreminder", "url": "lib.js"},
{"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true}
],
"data": [
{"name": "ar.settings.json", "name": "ar.stepsarray.json"}
]
}

View File

@ -0,0 +1,58 @@
(function(back) {
// Load settings
var settings = require("activityreminder").loadSettings();
// Show the menu
E.showMenu({
"" : { "title" : "Activity Reminder" },
"< Back" : () => back(),
'Enable': {
value: !!settings.enabled,
format: v => v?"Yes":"No",
onchange: v => {
settings.enabled = v;
require("activityreminder").writeSettings(settings);
}
},
'Start hour': {
value: 9|settings.startHour,
min: 0, max: 24,
onchange: v => {
settings.startHour = v;
require("activityreminder").writeSettings(settings);
}
},
'End hour': {
value: 20|settings.endHour,
min: 0, max: 24,
onchange: v => {
settings.endHour = v;
require("activityreminder").writeSettings(settings);
}
},
'Max innactivity': {
value: 30|settings.maxInnactivityMin,
min: 15, max: 60,
onchange: v => {
settings.maxInnactivityMin = v;
require("activityreminder").writeSettings(settings);
}
},
'Dismiss delay': {
value: 10|settings.dismissDelayMin,
min: 5, max: 15,
onchange: v => {
settings.dismissDelayMin = v;
require("activityreminder").writeSettings(settings);
}
},
'Min steps': {
value: 50|settings.minSteps,
min: 10, max: 500,
onchange: v => {
settings.minSteps = v;
require("activityreminder").writeSettings(settings);
}
}
});
})

View File

@ -18,3 +18,6 @@
0.17: Moving alarm internals to 'sched' library
0.18: Cope with >1 identical alarm at once (#1667)
0.19: Ensure rescheduled alarms that already fired have 'last' reset
0.20: Use the new 'sched' factories to initialize new alarms/timers
0.21: Fix time reset after a day of week change (#1676)
0.22: Refactor some methods to scheduling library

View File

@ -1,28 +1,11 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
var alarms = require("sched").getAlarms();
// An array of alarm objects (see sched/README.md)
// time in ms -> { hrs, mins }
function decodeTime(t) {
t = 0|t; // sanitise
var hrs = 0|(t/3600000);
return { hrs : hrs, mins : Math.round((t-hrs*3600000)/60000) };
}
// time in { hrs, mins } -> ms
function encodeTime(o) {
return o.hrs*3600000 + o.mins*60000;
}
function formatTime(t) {
var o = decodeTime(t);
return o.hrs+":"+("0"+o.mins).substr(-2);
}
let alarms = require("sched").getAlarms();
function getCurrentTime() {
var time = new Date();
let time = new Date();
return (
time.getHours() * 3600000 +
time.getMinutes() * 60000 +
@ -39,7 +22,7 @@ function showMainMenu() {
// Timer img "\0"+atob("DhKBAP////MDDAwwMGGBzgPwB4AeAPwHOBhgwMMzDez////w")
// Alarm img "\0"+atob("FBSBAABgA4YcMPDGP8Zn/mx/48//PP/zD/8A//AP/wD/8A//AP/wH/+D//w//8AAAADwAAYA")
const menu = {
'': { 'title': 'Alarm/Timer' },
'': { 'title': /*LANG*/'Alarms&Timers' },
/*LANG*/'< Back' : ()=>{load();},
/*LANG*/'New Alarm': ()=>editAlarm(-1),
/*LANG*/'New Timer': ()=>editTimer(-1)
@ -48,10 +31,10 @@ function showMainMenu() {
var type,txt; // a leading space is currently required (JS error in Espruino 2v12)
if (alarm.timer) {
type = /*LANG*/"Timer";
txt = " "+formatTime(alarm.timer);
txt = " "+require("sched").formatTime(alarm.timer);
} else {
type = /*LANG*/"Alarm";
txt = " "+formatTime(alarm.t);
txt = " "+require("sched").formatTime(alarm.t);
}
if (alarm.rp) txt += "\0"+atob("FBaBAAABgAAcAAHn//////wAHsABzAAYwAAMAADAAAAAAwAAMAADGAAzgAN4AD//////54AAOAABgAA=");
// rename duplicate alarms
@ -76,13 +59,13 @@ function showMainMenu() {
function editDOW(dow, onchange) {
const menu = {
'': { 'title': /*LANG*/'Days of Week' },
'< Back' : () => onchange(dow)
/*LANG*/'< Back' : () => onchange(dow)
};
for (var i = 0; i < 7; i++) (i => {
var dayOfWeek = require("locale").dow({ getDay: () => i });
for (let i = 0; i < 7; i++) (i => {
let dayOfWeek = require("locale").dow({ getDay: () => i });
menu[dayOfWeek] = {
value: !!(dow&(1<<i)),
format: v => v ? "Yes" : "No",
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => v ? dow |= 1<<i : dow &= ~(1<<i),
};
})(i);
@ -90,23 +73,15 @@ function editDOW(dow, onchange) {
}
function editAlarm(alarmIndex, alarm) {
var newAlarm = alarmIndex<0;
var a = {
t : 12*3600000, // 12 o clock default
on : true,
rp : false, // repeat not the default
as : false,
dow : 0b1111111,
last : 0,
vibrate : ".."
}
let newAlarm = alarmIndex < 0;
let a = require("sched").newDefaultAlarm();
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
if (alarm) Object.assign(a,alarm);
var t = decodeTime(a.t);
let t = require("sched").decodeTime(a.t);
const menu = {
'': { 'title': /*LANG*/'Alarm' },
'< Back' : () => showMainMenu(),
/*LANG*/'< Back' : () => showMainMenu(),
/*LANG*/'Hours': {
value: t.hrs, min : 0, max : 23, wrap : true,
onchange: v => t.hrs=v
@ -117,27 +92,31 @@ function editAlarm(alarmIndex, alarm) {
},
/*LANG*/'Enabled': {
value: a.on,
format: v=>v?"On":"Off",
format: v => v ? /*LANG*/"On" : /*LANG*/"Off",
onchange: v=>a.on=v
},
/*LANG*/'Repeat': {
value: a.rp,
format: v=>v?"Yes":"No",
onchange: v=>a.rp=v
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => a.rp = v
},
/*LANG*/'Days': {
value: "SMTWTFS".split("").map((d,n)=>a.dow&(1<<n)?d:".").join(""),
onchange: () => editDOW(a.dow, d=>{a.dow=d;editAlarm(alarmIndex,a)})
onchange: () => editDOW(a.dow, d => {
a.dow = d;
a.t = encodeTime(t);
editAlarm(alarmIndex, a);
})
},
/*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ),
/*LANG*/'Auto snooze': {
/*LANG*/'Auto Snooze': {
value: a.as,
format: v=>v?"Yes":"No",
onchange: v=>a.as=v
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => a.as = v
}
};
menu[/*LANG*/"Save"] = function() {
a.t = encodeTime(t);
a.t = require("sched").encodeTime(t);
a.last = (a.t < getCurrentTime()) ? (new Date()).getDate() : 0;
if (newAlarm) alarms.push(a);
else alarms[alarmIndex] = a;
@ -155,23 +134,15 @@ function editAlarm(alarmIndex, alarm) {
}
function editTimer(alarmIndex, alarm) {
var newAlarm = alarmIndex<0;
var a = {
timer : 5*60*1000, // 5 minutes
on : true,
rp : false,
as : false,
dow : 0b1111111,
last : 0,
vibrate : ".."
}
let newAlarm = alarmIndex < 0;
let a = require("sched").newDefaultTimer();
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
if (alarm) Object.assign(a,alarm);
var t = decodeTime(a.timer);
let t = require("sched").decodeTime(a.timer);
const menu = {
'': { 'title': /*LANG*/'Timer' },
'< Back' : () => showMainMenu(),
/*LANG*/'< Back' : () => showMainMenu(),
/*LANG*/'Hours': {
value: t.hrs, min : 0, max : 23, wrap : true,
onchange: v => t.hrs=v
@ -182,13 +153,13 @@ function editTimer(alarmIndex, alarm) {
},
/*LANG*/'Enabled': {
value: a.on,
format: v=>v?"On":"Off",
onchange: v=>a.on=v
format: v => v ? /*LANG*/"On" : /*LANG*/"Off",
onchange: v => a.on = v
},
/*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ),
};
menu[/*LANG*/"Save"] = function() {
a.timer = encodeTime(t);
a.timer = require("sched").encodeTime(t);
a.t = getCurrentTime() + a.timer;
a.last = 0;
if (newAlarm) alarms.push(a);

View File

@ -1,8 +1,8 @@
{
"id": "alarm",
"name": "Alarm & Timer",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.19",
"version": "0.22",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm,widget",

View File

@ -1,7 +1,7 @@
/* 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. */
E.showMessage("Updating boot0...");
E.showMessage(/*LANG*/"Updating boot0...");
var s = require('Storage').readJSON('setting.json',1)||{};
var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2
var boot = "", bootPost = "";
@ -209,7 +209,7 @@ delete bootPost;
delete bootFiles;
delete fileSize;
delete fileOffset;
E.showMessage("Reloading...");
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'

View File

@ -3,3 +3,4 @@
0.03: Adapt colors based on the theme of the user.
0.04: Steps can be hidden now such that the time is even larger.
0.05: Included icons for information.
0.06: Design and usability improvements.

View File

@ -1,13 +1,13 @@
# Black & White clock
# BW Clock
![](screenshot.png)
## Features
- Fullscreen on/off
- The design is adapted to the theme of your bangle.
- Tab left/right of screen to show steps, temperature etc.
- Enable / disable lock icon in the settings.
- If the "sched" app is installed tab top / bottom of the screen to set the timer.
- The design is adapted to the theme of your bangle.
## Thanks to
<a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>

View File

@ -99,21 +99,36 @@ var imgCharging = {
buffer : require("heatshrink").decompress(atob("//+v///k///4AQPwBANgBoMxBoMb/P+h/w/kH8H4gfB+EBwfggHH4EAt4CBn4CBj4CBh4FCCIO/8EB//Agf/wEH/8Gh//x////fAQIA="))
};
var imgWatch = {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/8B//+ARANB/l4//5/1/+f/n/n5+fAQnf9/P44CC8/n7/n+YOB/+fDQQgCEwQsCHBBEC"))
};
/*
* Draw timeout
* INFO ENTRIES
*/
// timeout used to update every minute
var drawTimeout;
var infoArray = [
function(){ return [ null, null, "left" ] },
function(){ return [ "Bangle", imgWatch, "right" ] },
function(){ return [ E.getBattery() + "%", imgBattery, "left" ] },
function(){ return [ getSteps(), imgSteps, "left" ] },
function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm, "left"] },
function(){ return [ getWeather().temp, imgTemperature, "left" ] },
function(){ return [ getWeather().wind, imgWind, "left" ] },
];
const NUM_INFO=infoArray.length;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
function getInfoEntry(){
if(isAlarmEnabled()){
return [getAlarmMinutes() + " min.", imgTimer, "left"]
} else if(Bangle.isCharging()){
return [E.getBattery() + "%", imgCharging, "left"]
} else{
return infoArray[settings.showInfo]();
}
}
@ -121,19 +136,21 @@ function queueDraw() {
* Helper
*/
function getSteps() {
var steps = 0;
try{
if (WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
steps = WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
return WIDGETS.activepedom.getSteps();
steps = WIDGETS.activepedom.getSteps();
} else {
return Bangle.getHealthStatus("day").steps;
steps = Bangle.getHealthStatus("day").steps;
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
}
return 0;
steps = Math.round(steps/100) / 10; // This ensures that we do not show e.g. 15.0k and 15k instead
return steps + "k";
}
@ -225,111 +242,109 @@ function decreaseAlarm(){
/*
* D R A W
* DRAW functions
*/
function draw() {
// queue draw in one minute
// Queue draw again
queueDraw();
// Set info
var showInfo = settings.showInfo;
if(isAlarmEnabled()){
showInfo = 100;
}
if(Bangle.isCharging()){
showInfo = 101;
}
// Draw clock
drawDate();
drawTime();
drawLock();
drawWidgets();
}
function drawDate(){
// Draw background
var yOffset = settings.fullscreen ? 0 : 10;
var y = H/5*2 + yOffset;
var y = H/5*2 + (settings.fullscreen ? 0 : 8);
g.reset().clearRect(0,0,W,W);
g.setColor(g.theme.fg);
g.fillRect(0,y,W,H);
// Draw date
y -= settings.fullscreen ? 5 : 0;
y -= settings.fullscreen ? 8 : 0;
var date = new Date();
g.setColor(g.theme.fg);
g.setFontAlign(1,1);
g.setMediumFont();
var dateStr = date.getDate();
dateStr = ("0" + dateStr).substr(-2);
g.drawString(dateStr, W/2-1, y+4);
g.setMediumFont(); // Needed to compute the width correctly
var dateW = g.stringWidth(dateStr);
g.setSmallFont();
var dayStr = locale.dow(date, true);
var monthStr = locale.month(date, 1);
var dayW = Math.max(g.stringWidth(dayStr), g.stringWidth(monthStr));
var fullDateW = dateW + 10 + dayW;
g.setFontAlign(-1,1);
g.drawString(locale.dow(date, true), W/2 + 10, y-23);
g.drawString(locale.month(date, 1), W/2 + 10, y+1);
g.setMediumFont();
g.setColor(g.theme.fg);
g.drawString(dateStr, W/2 - fullDateW / 2, y+5);
g.setSmallFont();
g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+3);
g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-23);
}
function drawTime(){
// Draw background
var y = H/5*2 + (settings.fullscreen ? 0 : 8);
g.setColor(g.theme.fg);
g.fillRect(0,y,W,H);
var date = new Date();
// Draw time
g.setColor(g.theme.bg);
g.setFontAlign(0,-1);
var timeStr = locale.time(date,1);
y += settings.fullscreen ? 20 : 10;
y += settings.fullscreen ? 14 : 10;
if(showInfo == 0){
y += 8;
var infoEntry = getInfoEntry();
var infoStr = infoEntry[0];
var infoImg = infoEntry[1];
var printImgLeft = infoEntry[2] == "left";
// Show large or small time depending on info entry
if(infoStr == null){
y += 10;
g.setLargeFont();
} else {
g.setMediumFont();
}
g.drawString(timeStr, W/2, y);
// Draw info or timer
y += H/5*2-5;
g.setFontAlign(0,0);
if(showInfo > 0){
g.setSmallFont();
var infoStr = "";
var infoImg;
if(showInfo == 100){
infoStr = getAlarmMinutes() + " min.";
infoImg = imgTimer;
} else if(showInfo == 101){
infoStr = E.getBattery() + "%";
infoImg = imgCharging;
} else if (showInfo == 1){
infoStr = E.getBattery() + "%";
infoImg = imgBattery;
} else if (showInfo == 2){
infoStr = getSteps()
infoStr = Math.round(infoStr/100) / 10; // This ensures that we do not show e.g. 15.0k and 15k instead
infoStr = infoStr + "k";
infoImg = imgSteps;
} else if (showInfo == 3){
infoStr = Math.round(Bangle.getHealthStatus("day").bpm) + " bpm";
infoImg = imgBpm;
} else if (showInfo == 4){
var weather = getWeather();
infoStr = weather.temp;
infoImg = imgTemperature;
} else if (showInfo == 5){
var weather = getWeather();
infoStr = weather.wind;
infoImg = imgWind;
// Draw info if set
if(infoStr == null){
return;
}
y += H/5*2-5;
g.setFontAlign(0,0);
g.setSmallFont();
var imgWidth = 0;
if(infoImg !== undefined){
imgWidth = infoImg.width;
var strWidth = g.stringWidth(infoStr);
g.drawImage(infoImg, W/2 - strWidth/2 - infoImg.width/2 - 5, y - infoImg.height/2);
}
g.drawString(infoStr, W/2 + imgWidth/2, y+3);
g.drawImage(
infoImg,
W/2 + (printImgLeft ? -strWidth/2-2 : strWidth/2+2) - infoImg.width/2,
y - infoImg.height/2
);
}
g.drawString(infoStr, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3);
}
// Draw lock
function drawLock(){
if(settings.showLock && Bangle.isLocked()){
g.setColor(g.theme.fg);
g.drawImage(imgLock, W-16, 2);
}
}
// Draw widgets if not fullscreen
function drawWidgets(){
if(settings.fullscreen){
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
} else {
@ -337,6 +352,27 @@ function draw() {
}
}
/*
* Draw timeout
*/
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, 60000 - (Date.now() % 60000));
}
/*
* Load clock, widgets and listen for events
*/
Bangle.loadWidgets();
// Clear the screen once, at startup and set the correct theme.
@ -381,32 +417,34 @@ Bangle.on('touch', function(btn, e){
if(is_upper){
Bangle.buzz(40, 0.6);
increaseAlarm();
draw();
drawTime();
}
if(is_lower){
Bangle.buzz(40, 0.6);
decreaseAlarm();
draw();
drawTime();
}
var maxInfo = 6;
if(is_right){
Bangle.buzz(40, 0.6);
settings.showInfo = (settings.showInfo+1) % maxInfo;
storage.write(SETTINGS_FILE, settings);
draw();
settings.showInfo = (settings.showInfo+1) % NUM_INFO;
drawTime();
}
if(is_left){
Bangle.buzz(40, 0.6);
settings.showInfo = settings.showInfo-1;
settings.showInfo = settings.showInfo < 0 ? maxInfo-1 : settings.showInfo;
storage.write(SETTINGS_FILE, settings);
draw();
settings.showInfo = settings.showInfo < 0 ? NUM_INFO-1 : settings.showInfo;
drawTime();
}
});
E.on("kill", function(){
storage.write(SETTINGS_FILE, settings);
});
// Show launcher when middle button pressed
Bangle.setUI("clock");

View File

@ -1,11 +1,11 @@
{
"id": "bwclk",
"name": "BlackWhite Clock",
"version": "0.05",
"description": "Black and white clock.",
"name": "BW Clock",
"version": "0.06",
"description": "BW Clock.",
"readme": "README.md",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}],
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot_2.png"}, {"url":"screenshot_3.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS2"],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
apps/bwclk/screenshot_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -18,7 +18,7 @@
E.showMenu({
'': { 'title': 'BlackWhite Clock' },
'': { 'title': 'BW Clock' },
'< Back': back,
'Fullscreen': {
value: settings.fullscreen,

View File

@ -1 +1,3 @@
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.

207
apps/choozi/appb2.js Normal file
View File

@ -0,0 +1,207 @@
/* 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();
}
});

View File

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

View File

@ -244,7 +244,7 @@ function run(){
Bangle.setLCDMode();
g.clear();
g.flip();
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
load(app.src);
}

View File

@ -9,3 +9,4 @@
0.09: fix the trasparent widget bar if there are no widgets for Bangle 2
0.10: added "one click exit" setting for Bangle 2
0.11: Fix bangle.js 1 white icons not displaying
0.12: On Bangle 2 change to swiping up/down to move between pages as to match page indicator. Swiping from left to right now loads the clock.

View File

@ -85,18 +85,25 @@ function drawPage(p){
g.flip();
}
Bangle.on("swipe",(dir)=>{
Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
selected = 0;
oldselected=-1;
if (dir<0){
if (dirUpDown==-1){
++page; if (page>maxPage) page=0;
drawPage(page);
} else {
} else if (dirUpDown==1){
--page; if (page<0) page=maxPage;
drawPage(page);
}
if (dirLeftRight==1) showClock();
});
function showClock(){
var app = require("Storage").readJSON('setting.json', 1).clock;
if (app) load(app);
else E.showMessage("clock\nnot found");
}
function isTouched(p,n){
if (n<0 || n>3) return false;
var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF;

View File

@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"name": "Desktop Launcher",
"version": "0.11",
"version": "0.12",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",

1
apps/golfview/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App! Very limited course support.

21
apps/golfview/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Jason Dekarske
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

33
apps/golfview/README.md Normal file
View File

@ -0,0 +1,33 @@
# Golf View
This app leverages open source map data to give you a birds eye view of your golf game! See a preview of any hole as well as your realtime distance to the green and position on the hole.
![hole3](screenshot.png)
## Usage
Swipe left and right to select your hole. Use the GPS assist app to get a faster GPS fix.
## Contributions
The performance of this app depends on the accuracy and consistency of user-submitted maps.
- See official mapping guidelines [here](https://wiki.openstreetmap.org/wiki/Tag:leisure%3Dgolf_course).
- All holes and features must be within the target course's area.
- Supported features are greens, fairways, tees, bunkers, water hazards and holes.
- All features for a given hole should have the "ref" tag with the hole number as value. Shared features should list ref values separated by ';'. [example](https://www.openstreetmap.org/way/36896320).
- here must be 18 holes and they must have the following tags: handicap, par, ref, dist.
- For any mapping assistance or issues, please file in the <a href="https://github.com/espruino/BangleApps/issues/new?assignees=&labels=bug&template=bangle-bug-report-custom-form.yaml&title=[golfview]+Short+description+of+bug">official repo</a>.
[Example Course](https://www.openstreetmap.org/way/25447898)
## Controls
Swipe to change holes and tap to see a green closeup.
## Requests/Creator
[Jason Dekarske](https://github.com/jdekarske)
## Attribution
[© OpenStreetMap contributors](https://www.openstreetmap.org/copyright)

154
apps/golfview/custom.html Normal file
View File

@ -0,0 +1,154 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<script src="https://cdn.jsdelivr.net/gh/mourner/simplify-js@1.2.4/simplify.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>
<body>
<div>
<input type="text" placeholder="Course ID" id="course_id">
<button type="button" onclick="courseSearch();">Search</button>
<p id="status"></p>
<div>
<button id="upload" class="btn btn-primary" disabled="true">Upload to Device</button>
<button id="download" class="btn btn-primary" disabled="true">Download Course</button>
</div>
<p>A course needs a few things to be parsed correctly by this tool.</p>
<ul>
<li>See official mapping guidelines <a
href="https://wiki.openstreetmap.org/wiki/Tag:leisure%3Dgolf_course">here</a>.</li>
<li>All holes and features must be within the target course's area.</li>
<li>Supported features are greens, fairways, tees, bunkers, water hazards and holes.</li>
<li>All features for a given hole should have the "ref" tag with the hole number as value. Shared features should
list ref values separated by ';'. <a href="https://www.openstreetmap.org/way/36896320">example</a>.</li>
<li>There must be 18 holes and they must have the following tags: handicap, par, ref, dist</li>
<li>For any mapping assistance or issues, please file in the <a
href="https://github.com/espruino/BangleApps/issues/new?assignees=&labels=bug&template=bangle-bug-report-custom-form.yaml&title=[golfview]+Short+description+of+bug">official
repo</a></li>
</ul>
<a href="https://www.openstreetmap.org/way/25447898">Example Course</a>
<a href="https://www.openstreetmap.org/copyright">© OpenStreetMap contributors</p>
</div>
<script src="../../core/lib/customize.js"></script>
<script src="./maptools.js"></script>
<script>
const url = "https://overpass-api.de/api/interpreter";
let query = `[out:json][timeout:5];way(25447898);map_to_area ->.golfcourse;way["golf"="hole"](area.golfcourse)->.holes;(relation["golf"="fairway"](area.golfcourse);way["golf"~"^(green|tee|water_hazard|bunker|fairway)"](area.golfcourse);)->.features;.holes out geom;.features out geom;`;
let course_input = null;
function courseSearch() {
let inputVal = document.getElementById("course_id").value;
query = `[out:json][timeout:5];way(${inputVal});map_to_area ->.golfcourse;way["golf"="hole"](area.golfcourse)->.holes;(relation["golf"="fairway"](area.golfcourse);way["golf"~"^(green|tee|water_hazard|bunker|fairway)"](area.golfcourse);)->.features;.holes out geom;.features out geom;`;
doQuery();
}
function processFeatures(course_verbose) {
let course_processed = {
holes: {}
};
for (let i = 0; i < course_verbose.length; i++) {
const element = course_verbose[i];
if (element.tags.golf === "hole") {
// if we find a high-level hole feature
// todo check if hole exists
let current_hole = parseInt(element.tags.ref); //subsequent way features should be applied to the current hole
let tees = []
Object.keys(element.tags).forEach((key) => {
if (key.includes("dist")) {
tees.push(Math.round(element.tags[key]));
}
})
var hole = {
hole_number: current_hole,
handicap: parseInt(element.tags.handicap),
par: parseInt(element.tags.par),
nodesXY: preprocessCoords(element.geometry, element.geometry[0]),
tees: tees.sort(),
way: element.geometry,
features: [],
angle: 0,
}
hole.angle = angle(hole.nodesXY[0], hole.nodesXY[hole.nodesXY.length - 1])
course_processed.holes[current_hole.toString()] = hole;
}
else {
if (!("ref" in element.tags)) continue;
if (element.type === "relation") {
for (member of element.members) {
if (member.role === "outer") break; // only use the outer because it is overwritten anyway
}
Object.assign(element, { "geometry": member.geometry });
}
// if we find a feature add it to the corresponding hole
let active_holes = element.tags.ref.split(";"); // a feature can be on more than one hole
for (feature_hole of active_holes) {
let new_feature = {
nodesXY: preprocessCoords(element.geometry, course_processed.holes[feature_hole].way[0]),
type: element.tags.golf,
id: element.id,
}
course_processed.holes[feature_hole].features.push(new_feature);
}
}
}
return course_processed;
}
function preprocessCoords(coord_array, origin) {
let many_points = arraytoXY(coord_array, origin);
let less_points = simplify(many_points, 2, true); // from simplify-js
// convert to int to save some memory
less_points = less_points.map(function (pt) {
return { x: Math.round(pt.x), y: Math.round(pt.y) }
});
return less_points;
}
var courses = [];
var course_name = "Davis";
$("#upload").click(function () {
sendCustomizedApp({
storage: courses,
});
});
$("#download").click(function () {
downloadObjectAsJSON(courses[0].content, "golfcourse-" + course_name);
});
// download info from the course
function doQuery() {
$.post(url, query, function (result) {
if (result.elements.length === 0) {
$('#status').text("Course not found!");
return;
}
course_input = result;
console.log(course_input);
out = processFeatures(course_input.elements);
console.log(out);
courses.push({
name: "golfcourse-" + course_name + ".json",
content: JSON.stringify(out),
});
$('#status').text("Course retrieved!");
$('#upload').attr("disabled", false);
$('#download').attr("disabled", false);
})
}
</script>
</body>
</html>

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("lEowcF23btoCRpMkyVKBxHayQOCpNbBw/UBwkrBw21BYQCCkoOIy/JCIetI4+X+QgEBxNIBxFqBxFWBxAsEBxHpBYPbsgOFqQOEy3btgOLDoIOrrYOKMoQOKOgYsKBx1KBwaGBWYgOBkVJBwKGBYQwOBiVJlIdCkmVBxdZfwwOGF4IONkoLCB2J3BBxkgQwQOKWYlJlYOUtQORtskzQOHtoOE7QOLAQdbBw21BydKBYgCD6gODBYwCNA"))

212
apps/golfview/golfview.js Normal file
View File

@ -0,0 +1,212 @@
// maptools.js
const EARTHRADIUS = 6371000; //km
function radians(a) {
return a * Math.PI / 180;
}
function degrees(a) {
let d = a * 180 / Math.PI;
return (d + 360) % 360;
}
function toXY(a, origin) {
let pt = {
x: 0,
y: 0
};
pt.x = EARTHRADIUS * radians(a.lon - origin.lon) * Math.cos(radians((a.lat + origin.lat) / 2));
pt.y = EARTHRADIUS * radians(origin.lat - a.lat);
return pt;
}
function arraytoXY(array, origin) {
let out = [];
for (var j in array) {
let newpt = toXY(array[j], origin);
out.push(newpt);
}
return out;
}
function angle(a, b) {
let x = b.x - a.x;
let y = b.y - a.y;
return Math.atan2(-y, x);
}
function rotateVec(a, theta) {
let pt = {
x: 0,
y: 0
};
c = Math.cos(theta);
s = Math.sin(theta);
pt.x = c * a.x - s * a.y;
pt.y = s * a.x + c * a.y;
return pt;
}
function distance(a, b) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
// golfview.js
let course = require("Storage").readJSON("golfcourse-Davis.json").holes;//TODO use the course ID
let current_hole = 1;
let hole = course[current_hole.toString()];
let user_position = {
fix: false,
lat: 0,
lon: 0,
x: 0,
y: 0,
to_hole: 0,
last_time: getTime(),
transform: {},
};
function drawUser() {
if(!user_position.fix) return;
let new_pos = g.transformVertices([user_position.x,user_position.y],user_position.transform);
g.setColor(g.theme.fg);
g.drawCircle(new_pos[0],new_pos[1],8);
}
function drawHole(l) {
//console.log(l);
let hole_straight_distance = distance(
hole.nodesXY[0],
hole.nodesXY[hole.nodesXY.length - 1]
);
let scale = 0.9 * l.h / hole_straight_distance;
let transform = {
x: l.x + l.w / 2, // center in the box
y: l.h * 0.95, // pad it just a bit TODO use the extent of the teeboxes/green
scale: scale, // scale factor (default 1)
rotate: hole.angle - Math.PI / 2.0, // angle in radians (default 0)
};
user_position.transform = transform;
// draw the fairways first
hole.features.sort((a, b) => {
if (a.type === "fairway") {
return -1;
}
});
for (var feature of hole.features) {
//console.log(Object.keys(feature));
if (feature.type === "fairway") {
g.setColor(1, 0, 1); // magenta
} else if (feature.type === "tee") {
g.setColor(1, 0, 0); // red
} else if (feature.type === "green") {
g.setColor(0, 1, 0); // green
} else if (feature.type === "bunker") {
g.setColor(1, 1, 0); // yellow
} else if (feature.type === "water_hazard") {
g.setColor(0, 0, 1); // blue
} else {
continue;
}
var nodelist = [];
feature.nodesXY.forEach(function (node) {
nodelist.push(node.x);
nodelist.push(node.y);
});
newnodelist = g.transformVertices(nodelist, transform);
g.fillPoly(newnodelist, true);
//console.log(feature.type);
//console.log(newnodelist);
drawUser();
}
var waynodelist = [];
hole.nodesXY.forEach(function (node) {
waynodelist.push(node.x);
waynodelist.push(node.y);
});
newnodelist = g.transformVertices(waynodelist, transform);
g.setColor(0, 1, 1); // cyan
g.drawPoly(newnodelist);
}
function setHole(current_hole) {
layout.hole.label = "HOLE " + current_hole;
layout.par.label = "PAR " + course[current_hole.toString()].par;
layout.hcp.label = "HCP " + course[current_hole.toString()].handicap;
layout.postyardage.label = course[current_hole.toString()].tees[course[current_hole.toString()].tees.length - 1]; //TODO only use longest hole for now
g.clear();
layout.render();
}
function updateDistanceToHole() {
let xy = toXY({ "lat": user_position.lat, "lon": user_position.lon }, hole.way[0]);
user_position.x = xy.x;
user_position.y = xy.y;
user_position.last_time = getTime();
let new_distance = Math.round(distance(xy, hole.nodesXY[hole.nodesXY.length - 1]) * 1.093613); //TODO meters later
//console.log(new_distance);
layout.measyardage.label = (new_distance < 999) ? new_distance : "---";
g.clear();
layout.render();
}
Bangle.on('swipe', function (direction) {
if (direction > 0) {
current_hole--;
} else {
current_hole++;
}
if (current_hole > 18) { current_hole = 1; }
if (current_hole < 1) { current_hole = 18; }
hole = course[current_hole.toString()];
setHole(current_hole);
});
Bangle.on('GPS', (fix) => {
if (isNaN(fix.lat)) return;
//console.log(fix.hdop * 5); //precision
user_position.fix = true;
user_position.lat = fix.lat;
user_position.lon = fix.lon;
updateDistanceToHole();
drawUser();
});
// The layout, referencing the custom renderer
var Layout = require("Layout");
var layout = new Layout({
type: "h", c: [
{
type: "v", c: [
{ type: "txt", font: "10%", id: "hole", label: "HOLE 18" },
{ type: "txt", font: "10%", id: "par", label: "PAR 4" },
{ type: "txt", font: "10%", id: "hcp", label: "HCP 18" },
{ type: "txt", font: "35%", id: "postyardage", label: "---" },
{ type: "txt", font: "20%", id: "measyardage", label: "---" },
]
},
{ type: "custom", render: drawHole, id: "graph", bgCol: g.theme.bg, fillx: 1, filly: 1 }
],
lazy: true
});
Bangle.setGPSPower(1);
setHole(current_hole);
//layout.debug();

BIN
apps/golfview/golfview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

63
apps/golfview/maptools.js Normal file
View File

@ -0,0 +1,63 @@
const EARTHRADIUS = 6371000; //km
function radians(a) {
return a * Math.PI / 180;
}
function degrees(a) {
let d = a * 180 / Math.PI;
return (d + 360) % 360;
}
function toXY(a, origin) {
let pt = {
x: 0,
y: 0
};
pt.x = EARTHRADIUS * radians(a.lon - origin.lon) * Math.cos(radians((a.lat + origin.lat) / 2));
pt.y = EARTHRADIUS * radians(origin.lat - a.lat);
return pt;
}
function arraytoXY(array, origin) {
let out = [];
for (var j in array) {
let newpt = toXY(array[j], origin);
out.push(newpt);
}
return out;
}
function angle(a, b) {
let x = b.x - a.x;
let y = b.y - a.y;
return Math.atan2(-y, x);
}
function rotateVec(a, theta) {
let pt = {
x: 0,
y: 0
};
c = Math.cos(theta);
s = Math.sin(theta);
pt.x = c * a.x - s * a.y;
pt.y = s * a.x + c * a.y;
return pt;
}
function distance(a,b) {
return Math.sqrt(Math.pow(a.x-b.x,2) + Math.pow(a.y-b.y,2))
}
// https://stackoverflow.com/questions/19721439/download-json-object-as-a-file-from-browser
function downloadObjectAsJSON(exportObj, exportName) {
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(exportObj); // must be stringified!!
var downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", exportName + ".json");
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}

View File

@ -0,0 +1,15 @@
{ "id": "golfview",
"name": "Golf View",
"version":"0.01",
"description": "This app will provide you with on course data to support your golf game!",
"icon": "golfview.png",
"tags": "outdoors, gps",
"allow_emulator": true,
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"custom": "custom.html",
"storage": [
{"name":"golfview.app.js","url":"golfview.js"},
{"name":"golfview.img","url":"golfview-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -5,7 +5,7 @@ function satelliteImage() {
var Layout = require("Layout");
var layout;
//Bangle.setGPSPower(1, "app");
E.showMessage("Loading..."); // avoid showing rubbish on screen
E.showMessage(/*LANG*/"Loading..."); // avoid showing rubbish on screen
var lastFix = {
fix: -1,

View File

@ -126,7 +126,7 @@ function asTime(v){
function viewTrack(n, info) {
if (!info) {
E.showMessage("Loading...","GPS Track "+n);
E.showMessage(/*LANG*/"Loading...","GPS Track "+n);
info = getTrackInfo(n);
}
const menu = {

View File

@ -10,7 +10,7 @@ var Layout = require("Layout");
Bangle.setGPSPower(1, "app");
Bangle.loadWidgets();
Bangle.drawWidgets();
E.showMessage("Loading..."); // avoid showing rubbish on screen
E.showMessage(/*LANG*/"Loading..."); // avoid showing rubbish on screen
function setGPSTime() {
if (fix.time!==undefined) {

View File

@ -10,3 +10,4 @@
0.09: Fix file naming so months are 1-based (not 0) (fix #1119)
0.10: Adds additional 3 minute setting for HRM
0.11: Pre-minified boot&lib - folds constants and saves RAM
0.12: Add setting for Daily Step Goal

View File

@ -24,6 +24,7 @@ Stores:
* **Off** - Don't turn HRM on, but record heart rate if the HRM was turned on by another app/widget
* **10 Min** - Turn HRM on every 10 minutes (for each heath entry) and turn it off after 2 minutes, or when a good reading is found
* **Always** - Keep HRM on all the time (more accurate recording, but reduces battery life to ~36 hours)
* **Daily Step Goal** - Default 10000, daily step goal for pedometer apps to use
## Technical Info

View File

@ -2,8 +2,8 @@ function getSettings() {
return require("Storage").readJSON("health.json",1)||{};
}
function setSettings(s) {
require("Storage").writeJSON("health.json",s);
function setSettings(healthSettings) {
require("Storage").writeJSON("health.json",healthSettings);
}
function menuMain() {
@ -22,15 +22,21 @@ function menuMain() {
function menuSettings() {
swipe_enabled = false;
clearButton();
var s=getSettings();
var healthSettings=getSettings();
//print(healthSettings);
E.showMenu({
"":{title:"Health Tracking"},
"< Back":()=>menuMain(),
"Heart Rt":{
value : 0|s.hrm,
value : 0|healthSettings.hrm,
min : 0, max : 3,
format : v=>["Off","3 mins","10 mins","Always"][v],
onchange : v => { s.hrm=v;setSettings(s); }
onchange : v => { healthSettings.hrm=v;setSettings(healthSettings); }
},
"Daily Step Goal":{
value : (healthSettings.stepGoal ? healthSettings.stepGoal : 10000),
min : 0, max : 20000, step : 100,
onchange : v => { healthSettings.stepGoal=v;setSettings(healthSettings); }
}
});
}
@ -70,7 +76,7 @@ function menuHRM() {
function stepsPerHour() {
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
g.clear(1);
@ -81,7 +87,7 @@ function stepsPerHour() {
}
function stepsPerDay() {
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
g.clear(1);
@ -92,7 +98,7 @@ function stepsPerDay() {
}
function hrmPerHour() {
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
var cnt = new Uint8Array(23);
require("health").readDay(new Date(), h=>{
@ -108,7 +114,7 @@ function hrmPerHour() {
}
function hrmPerDay() {
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
var cnt = new Uint8Array(31);
require("health").readDailySummaries(new Date(), h=>{
@ -124,7 +130,7 @@ function hrmPerDay() {
}
function movementPerHour() {
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
g.clear(1);
@ -135,7 +141,7 @@ function movementPerHour() {
}
function movementPerDay() {
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
g.clear(1);
@ -199,7 +205,7 @@ function drawBarChart() {
for (bar = 1; bar < 10; bar++) {
if (bar == 5) {
g.setFont('6x8', 2);
g.setFontAlign(0,-1)
g.setFontAlign(0,-1);
g.setColor(g.theme.fg);
g.drawString(chart_label + " " + (chart_index + bar -1) + " " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150);
g.setColor("#00f");

View File

@ -1,7 +1,7 @@
{
"id": "health",
"name": "Health Tracking",
"version": "0.11",
"version": "0.12",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"tags": "tool,system,health",

1
apps/heartzone/ChangeLog Normal file
View File

@ -0,0 +1 @@
1.0: Initial release.

35
apps/heartzone/README.md Normal file
View File

@ -0,0 +1,35 @@
# HeartZone
HeartZone continuously monitors your heart rate. If your heart rate is outside of your configured limits, you get a configurable buzz.
Inspired by [Workout HRM](https://github.com/espruino/BangleApps/tree/master/apps/wohrm), but I wanted the following features:
* Larger text, more contrast, and color-coding for better readability while exercising.
* Configurable buzz interval, instead of at every heart rate reading (which was too distracting).
* Pause for a rest and resume afterwards without having to restart the heart rate sensor (which takes several seconds each time to stabilize).
* Configure the minimum heart rate confidence threshold (bad readings cause buzzes that have to be ignored).
However, compared to Workout HRM, HeartZone doesn't support:
* In-app configuration of the heart rate thresholds - you can only do it in the Settings app.
* Bangle.js 1 - this only supports Bangle.js 2.
## Usage
When you first start the app, it will begin displaying your heart rate after a few seconds. Until the heart rate confidence is above your configured minimum confidence, the background will be colored red:
![Start screen](screenshots/start.png)
After the heart rate confidence is at an acceptable level, the background will be colored white, and you will receive buzzes on your wrist while your heart rate is out of the configured range. By default, the BPM-too-low buzz is 200ms, while the BPM-too-high buzz is 1000ms:
![Screen while we are monitoring](screenshots/running.png)
If you're taking a break, swipe down to turn off the buzzes while continuing to measure and display your heart rate (swipe up again to end your break):
![Screen while we are paused](screenshots/paused.png)
When you're done, simply press the side button to exit the app.
## Creator
[Uberi](https://github.com/Uberi)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkBiIA/AHqFCAxQWJ5gABCIQGGABEQB4QABgIGGC5MMCAnAAwwuOABAwIC64/FABBIIC68ADBnAVJEP+AXLBoJ2H/4XN/54GBAIXOGAouBBAMAABQXBGAoHCAB4wDFwQARGAYvWL7CPDbBXAR46/DiAXJgK/Id4URGBHABobwHEAIwIBQQuHAAcYGA3AwIUKC4eAC4sIC5+IGAnAxAXQkAXDgQXRkQwC4EiC6QwCgQXTl0M4HiC6nghwXV93uC9MRC44WOGAIXFFx4ABC4oWQiMSC4chC6MRC4YWSiMeC4PhC6cRC4IWUGAIuVAH4AVA="))

87
apps/heartzone/app.js Normal file
View File

@ -0,0 +1,87 @@
// clear screen and draw widgets
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
var statusRect = {x: Bangle.appRect.x, y: Bangle.appRect.y, w: Bangle.appRect.w, h: 32};
var settingsRect = {x: Bangle.appRect.x, y: Bangle.appRect.y2 - 16, w: Bangle.appRect.w, h: 16};
var hrmRect = {x: Bangle.appRect.x, y: statusRect.y + statusRect.h, w: Bangle.appRect.w, h: Bangle.appRect.h - statusRect.h - settingsRect.h};
var isPaused = false;
var settings = Object.assign({
minBpm: 120,
maxBpm: 160,
minConfidence: 60,
minBuzzIntervalSeconds: 5,
tooLowBuzzDurationMillis: 200,
tooHighBuzzDurationMillis: 1000,
}, require('Storage').readJSON("heartzone.settings.json", true) || {});
// draw current settings at the bottom
g.setFont6x15(1).setFontAlign(0, -1, 0);
g.drawString(settings.minBpm + "<BPM<" + settings.maxBpm + ", >=" + settings.minConfidence + "% conf.", settingsRect.x + (settingsRect.w / 2), settingsRect.y + 4);
function drawStatus(status) { // draw status bar at the top
g.setBgColor(g.theme.bg).setColor(g.theme.fg);
g.clearRect(statusRect);
g.setFontVector(statusRect.h - 4).setFontAlign(0, -1, 0);
g.drawString(status, statusRect.x + (statusRect.w / 2), statusRect.y + 2);
}
function drawHRM(hrmInfo) { // draw HRM info display
g.setBgColor(hrmInfo.confidence > settings.minConfidence ? '#fff' : '#f00').setColor(hrmInfo.confidence > settings.minConfidence ? '#000' : '#fff');
g.setFontAlign(-1, -1, 0);
g.clearRect(hrmRect);
var px = hrmRect.x + 10, py = hrmRect.y + 10;
g.setFontVector((hrmRect.h / 2) - 20);
g.drawString(hrmInfo.bpm, px, py);
g.setFontVector(16);
g.drawString('BPM', px + g.stringWidth(hrmInfo.bpm.toString()) + 32, py);
py += hrmRect.h / 2;
g.setFontVector((hrmRect.h / 2) - 20);
g.drawString(hrmInfo.confidence, px, py);
g.setFontVector(16);
g.drawString('% conf.', px + g.stringWidth(hrmInfo.confidence.toString()) + 32, py);
}
drawHRM({bpm: '?', confidence: '?'});
drawStatus('RUNNING');
var lastBuzz = getTime();
Bangle.on('HRM', function(hrmInfo) {
if (!isPaused) {
var currentTime;
if (hrmInfo.confidence > settings.minConfidence) {
if (hrmInfo.bpm < settings.minBpm) {
currentTime = getTime();
if (currentTime - lastBuzz > settings.minBuzzIntervalSeconds) {
lastBuzz = currentTime;
Bangle.buzz(settings.tooLowBuzzDurationMillis);
}
} else if (hrmInfo.bpm > settings.maxBpm) {
currentTime = getTime();
if (currentTime - lastBuzz > minBuzzIntervalSeconds) {
lastBuzz = currentTime;
Bangle.buzz(settings.tooHighBuzzDurationMillis);
}
}
}
}
drawHRM(hrmInfo);
});
Bangle.setUI('updown', function(action) {
if (action == -1) { // up
isPaused = false;
drawStatus("RUNNING");
} else if (action == 1) { // down
isPaused = true;
drawStatus("PAUSED");
}
});
setWatch(function() { Bangle.setHRMPower(false, "heartzone"); load(); }, BTN1);
Bangle.setHRMPower(true, "heartzone");

BIN
apps/heartzone/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,21 @@
{
"id": "heartzone",
"name": "HeartZone",
"version": "1.0",
"description": "Exercise app for keeping your heart rate in the aerobic zone. Buzzes the watch at configurable intervals when your heart rate is outside of configured limits.",
"readme":"README.md",
"screenshots": [
{"url": "screenshots/start.png"},
{"url": "screenshots/running.png"},
{"url": "screenshots/paused.png"}
],
"icon": "icon.png",
"tags": "health",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"heartzone.app.js","url":"app.js"},
{"name":"heartzone.settings.js","url":"settings.js"},
{"name":"heartzone.img","url":"app-icon.js","evaluate":true}
],
"data": [{"name":"heartzone.settings.json"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,27 @@
(function(back) {
var FILE = "heartzone.settings.json";
var settings = Object.assign({
minBpm: 120,
maxBpm: 160,
minConfidence: 60,
minBuzzIntervalSeconds: 5,
tooLowBuzzDurationMillis: 200,
tooHighBuzzDurationMillis: 1000,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// Show the menu
E.showMenu({
"" : { "title" : "HeartZone" },
"< Save & Return" : () => { writeSettings(); back(); },
'Min BPM': {value: 0 | settings.minBpm, min: 80, max: 200, step: 10, onchange: v => { settings.minBpm = v; }},
'Max BPM': {value: 0 | settings.maxBpm, min: 80, max: 200, step: 10, onchange: v => { settings.maxBpm = v; }},
'Min % conf.': {value: 0 | settings.minConfidence, min: 30, max: 100, step: 5, onchange: v => { settings.minConfidence = v; }},
'Min buzz int. (sec)': {value: 0 | settings.minBuzzIntervalSeconds, min: 1, max: 30, onchange: v => { settings.minBuzzIntervalSeconds = v; }},
'BPM too low buzz (ms)': {value: 0 | settings.tooLowBuzzDurationMillis, min: 0, max: 3000, step: 100, onchange: v => { settings.tooLowBuzzDurationMillis = v; }},
'BPM too high buzz (ms)': {value: 0 | settings.tooHighBuzzDurationMillis, min: 0, max: 3000, step: 100, onchange: v => { settings.tooHighBuzzDurationMillis = v; }},
});
})

View File

@ -105,8 +105,10 @@ E.on('notify',msg=>{
"io.robbie.HomeAssistant": "Home Assistant",
"net.weks.prowl": "Prowl",
"net.whatsapp.WhatsApp": "WhatsApp",
"net.superblock.Pushover": "Pushover",
"nl.ah.Appie": "Albert Heijn",
"nl.postnl.TrackNTrace": "PostNL",
"org.whispersystems.signal": "Signal",
"ph.telegra.Telegraph": "Telegram",
"tv.twitch": "Twitch",

View File

@ -1 +1,2 @@
0.01: New App!
0.02: Now keeps user input trace intact by changing how the screen is updated.

View File

@ -45,11 +45,39 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
var flashToggle = false;
const R = Bangle.appRect;
var Rx1;
var Rx2;
var Ry1;
var Ry2;
function findMarker(strArr) {
if (strArr.length == 0) {
Rx1 = 4;
Rx2 = 6*4;
Ry1 = 8*4;
Ry2 = 8*4 + 3;
} else if (strArr.length <= 4) {
Rx1 = (strArr[strArr.length-1].length)%7*6*4 + 4 ;
Rx2 = (strArr[strArr.length-1].length)%7*6*4 + 6*4;
Ry1 = (strArr.length)*(8*4) + Math.floor((strArr[strArr.length-1].length)/7)*(8*4);
Ry2 = (strArr.length)*(8*4) + Math.floor((strArr[strArr.length-1].length)/7)*(8*4) + 3;
} else {
Rx1 = (strArr[strArr.length-1].length)%7*6*4 + 4 ;
Rx2 = (strArr[strArr.length-1].length)%7*6*4 + 6*4;
Ry1 = (4)*(8*4) + Math.floor((strArr[strArr.length-1].length)/7)*(8*4);
Ry2 = (4)*(8*4) + Math.floor((strArr[strArr.length-1].length)/7)*(8*4) + 3;
}
//print(Rx1,Rx2,Ry1, Ry2);
return {x:Rx1,y:Ry1,x2:Rx2,y2:Ry2};
}
function draw(noclear) {
g.reset();
if (!noclear) g.clearRect(R);
var l = g.setFont("6x8:4").wrapString(text+(flashToggle?"_":" "), R.w-8);
var l = g.setFont("6x8:4").wrapString(text+' ', R.w-8);
if (!l) l = [];
//print(text+':');
//print(l);
if (!noclear) (flashToggle?(g.fillRect(findMarker(l))):(g.clearRect(findMarker(l))));
if (l.length>4) l=l.slice(-4);
g.drawString(l.join("\n"),R.x+4,R.y+4);
}
@ -80,6 +108,7 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
var ch = o.stroke;
if (ch=="\b") text = text.slice(0,-1);
else text += ch;
g.clearRect(R);
}
flashToggle = true;
draw();
@ -87,7 +116,7 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
Bangle.on('stroke',strokeHandler);
g.reset().clearRect(R);
show();
draw(true);
draw(false);
var flashInterval;
return new Promise((resolve,reject) => {

View File

@ -1,6 +1,6 @@
{ "id": "kbswipe",
"name": "Swipe keyboard",
"version":"0.01",
"version":"0.02",
"description": "A library for text input via PalmOS style swipe gestures (beta!)",
"icon": "app.png",
"type":"textinput",

View File

@ -12,3 +12,4 @@
0.11: Merge Bangle.js 1 and 2 launchers, again
0.12: Add an option to hide clocks from the app list (fix #1015)
Add /*LANG*/ tags for internationalisation
0.13: Add fullscreen mode

14
apps/launch/README.md Normal file
View File

@ -0,0 +1,14 @@
Launcher
========
This is the default launcher but you can replace it with a customised launcher.
The app is needed to display a menu with all the apps installed on your Bangle. You can launch an app by touching its name/icon.
Settings
--------
- `Font` - The font used (`4x6`, `6x8`, `12x20`, `6x15` or `Vector`). Default `12x20`.
- `Vector Font Size` - The size of the font if `Font` is set to `Vector`. Default `10`.
- `Show Clocks` - If set to `No` then clocks won't appear in the app list. Default `Yes`.
- `Fullscreen` - If set to `Yes` then widgets won't be loaded. Default `No`.

View File

@ -2,7 +2,10 @@ var s = require("Storage");
var scaleval = 1;
var vectorval = 20;
var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
let settings = Object.assign({ showClocks: true }, s.readJSON("launch.json", true) || {});
let settings = Object.assign({
showClocks: true,
fullscreen: false
}, s.readJSON("launch.json", true) || {});
if ("vectorsize" in settings) {
vectorval = parseInt(settings.vectorsize);
@ -44,8 +47,11 @@ function drawApp(i, r) {
}
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}
E.showScroller({
h : 64*scaleval, c : apps.length,

View File

@ -2,8 +2,9 @@
"id": "launch",
"name": "Launcher",
"shortName": "Launcher",
"version": "0.12",
"version": "0.13",
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
"readme": "README.md",
"icon": "app.png",
"type": "launch",
"tags": "tool,system,launcher",

View File

@ -1,6 +1,9 @@
// make sure to enclose the function in parentheses
(function(back) {
let settings = Object.assign({ showClocks: true }, require("Storage").readJSON("launch.json", true) || {});
let settings = Object.assign({
showClocks: true,
fullscreen: false
}, require("Storage").readJSON("launch.json", true) || {});
let fonts = g.getFonts();
function save(key, value) {
@ -8,7 +11,7 @@
require("Storage").write("launch.json",settings);
}
const appMenu = {
"": {"title": /*LANG*/"Launcher Settings"},
"": { "title": /*LANG*/"Launcher" },
/*LANG*/"< Back": back,
/*LANG*/"Font": {
value: fonts.includes(settings.font)? fonts.indexOf(settings.font) : fonts.indexOf("12x20"),
@ -16,15 +19,20 @@
onchange: (m) => {save("font", fonts[m])},
format: v => fonts[v]
},
/*LANG*/"Vector font size": {
/*LANG*/"Vector Font Size": {
value: settings.vectorsize || 10,
min:10, max: 20,step:1,wrap:true,
onchange: (m) => {save("vectorsize", m)}
},
/*LANG*/"Show clocks": {
/*LANG*/"Show Clocks": {
value: settings.showClocks == true,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: (m) => {save("showClocks", m)}
onchange: (m) => { save("showClocks", m) }
},
/*LANG*/"Fullscreen": {
value: settings.fullscreen == true,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: (m) => { save("fullscreen", m) }
}
};
E.showMenu(appMenu);

View File

@ -18,3 +18,5 @@
0.18: Fullscreen mode can now be enabled or disabled in the settings.
0.19: Alarms can not go bigger than 100.
0.20: Use alarm for alarm functionality instead of own implementation.
0.21: Add custom theming.
0.22: Fix alarm and add build in function for step counting.

View File

@ -1,8 +1,7 @@
# LCARS clock
A simple LCARS inspired clock.
Note: To display the steps, the wpedom app is required. To show weather data
such as temperature, humidity or window you BangleJS must be connected
To show weather data such as temperature, humidity or window you BangleJS must be connected
with Gadgetbride and the weather app must be installed. To use the timer
the "sched" app must be installed on your device.
@ -19,6 +18,7 @@ the "sched" app must be installed on your device.
* Tap on top/bottom of screen 1 to activate an alarm. Depends on widtmr.
* The lower orange line indicates the battery level.
* Display graphs (day or month) for steps + hrm on the second screen.
* Customizable theming colors in the settings menu of the app.
## Data that can be configured
* Steps - Steps loaded via the wpedom app.
@ -43,3 +43,4 @@ Access different screens via tap on the left/ right side of the screen
## Contributors
- [Adam Schmalhofer](https://github.com/adamschmalhofer)
- [Jon Warrington](https://github.com/BartokW)
- [Ronin Stegner](https://github.com/Ronin0000)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 B

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 B

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 B

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 769 B

After

Width:  |  Height:  |  Size: 742 B

View File

@ -1,14 +1,17 @@
const TIMER_IDX = "lcars";
const SETTINGS_FILE = "lcars.setting.json";
const locale = require('locale');
const storage = require('Storage');
const storage = require('Storage')
let settings = {
alarm: -1,
dataRow1: "Steps",
dataRow2: "Temp",
dataRow2: "HRM",
dataRow3: "Battery",
speed: "kph",
fullscreen: false,
themeColor1BG: "#FF9900",
themeColor2BG: "#FF00DC",
themeColor3BG: "#0094FF",
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@ -18,9 +21,9 @@ for (const key in saved_settings) {
/*
* Colors to use
*/
let cBlue = "#0094FF";
let cOrange = "#FF9900";
let cPurple = "#FF00DC";
let color1 = settings.themeColor3BG;
let color2 = settings.themeColor1BG;
let color3 = settings.themeColor2BG;
let cWhite = "#FFFFFF";
let cBlack = "#000000";
let cGrey = "#424242";
@ -33,33 +36,77 @@ let lcarsViewPos = 0;
var plotMonth = false;
/*
* Requirements and globals
*/
function convert24to16(input)
{
let RGB888 = parseInt(input.replace(/^#/, ''), 16);
let r = (RGB888 & 0xFF0000) >> 16;
let g = (RGB888 & 0xFF00) >> 8;
let b = RGB888 & 0xFF;
r = (r * 249 + 1014) >> 11;
g = (g * 253 + 505) >> 10;
b = (b * 249 + 1014) >> 11;
let RGB565 = 0;
RGB565 = RGB565 | (r << 11);
RGB565 = RGB565 | (g << 5);
RGB565 = RGB565 | b;
return "0x"+RGB565.toString(16);
}
var color1C = convert24to16(color1);
var color2C = convert24to16(color2);
var color3C = convert24to16(color3);
/*
* Requirements and globals
*/
var colorPalette = new Uint16Array([
0x0000, // not used
color2C, // second
color3C, // third
0x0000, // not used
color1C, // first
0x0000, // not used
0x0000, // not used
0x0000, // not used
0x0000, // not used
0x0000, // not used
0x0000, // not used
0x0000, // not used
0x0000, // not used
0x0000, // not used
0x0000, // not used
0x0000 // not used
],0,1);
var bgLeftFullscreen = {
width : 27, height : 176, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AAUM2XLlgCCwAJBBAuy4EAmQIF5cggAIGlmwgYIG2XIF42wF4ImGF4ImHJoQmGJoQdJhZNHNY47CgRNGBIJZHHgRiGBIRQ/KH5QCAFCh/eX5Q/KAwdCAGVbtu27YCCoAJBkuWrNlAQRGCiwRDAQPQBIMJCIYCBsAJBgomEtu0WoQmEy1YBIMBHYttIwQ7FyxQ/KHFlFAQ7F2weCHYplKChRTCCg5TCHw5TMAD0GzVp0wCCBBGaBIMaBAtpwECBA2mwEJBAugDgMmCIwJBF5EABAtoeQQvGCYQdPJoI7LMQzTCLJKAGzAJBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A=="))
buffer : require("heatshrink").decompress((atob("/4AB+VJkmSAQV///+BAtJn//5IIFkmf/4IGyVP/gIGpMnF41PHIImGF4ImHJoQmGJoIdK8hNHNY47C/JNGBIJZGyYJBQA5GCKH5Q/KAQAoUP7y/KH5QGDoQAy0hGF34JB6RGFr4JB9JkFl4JB+gdFy4JB/QdFpYJB/odFkqrCS4xGCWoyDCKH5Q1GShlJChQLCCg5TCHw5TMAD35FAoIIkgJB8hGGv/8Mg8/+QIFp4cB5IRGBIIvI/4IFybyCF4wTCDp5NBHZZiGz4JBLJKAGk4JBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4"))),
palette: colorPalette
};
var bgLeftNotFullscreen = {
width : 27, height : 152, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AAUM2XLlgCCwAJBBAuy4EAmQIF5cggAIGlmwgYIG2XIF42wF4ImGF4ImHJoQmGJoQdJhZNHNY47CgRNGBIJZHHgRiGBIRQ/KH5QCAGVbtu27YCCoAJBkuWrNlAQRkCiwRDAQPQBIMJCIYCBsAJBgomEtu0WoQmEy1YBIMBHYttIwQ7FyxQ/KHFlFAQ7F2weCHYplKChRTCCg5TCHw5TMAD0GzVp0wCCBBGaBIMaBAtpwECBA2mwEJBAugDgMmCIwJBF5EABAtoeQQvGCYQdPJoI7LMQzTCLJKAGzAJBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A="))
buffer : require("heatshrink").decompress((atob("/4AB+VJkmSAQV///+BAtJn//5IIFkmf/4IGyVP/gIGpMnF41PHIImGF4ImHJoQmGJoIdK8hNHNY47C/JNGBIJZGyYJBQA5GCKH5Q/KAQAy0hGF34JB6RGFr4JB9JkFl4JB+gdFy4JB/QdFpYJB/odFkqrCS4xGCWoyhCKH5Q1GShlJChQLCCg5TCHw5TMAD35FAoIIkgJB8hGGv/8Mg8/+QIFp4cB5IRGBIIvI/4IFybyCF4wTCDp5NBHZZiGz4JBLJKAGk4JBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A=="))),
palette: colorPalette
};
var bgRightFullscreen = {
width : 27, height : 176, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("lmy5YCDBIUyBAmy5AJBhYUG2EAhgIFAQMAgQIGCgQABCg4ABEAwUNFI2AKZHAKZEgGRZTGOIUDQxJxGKH5Q/agwAnUP7y/KH4yGeVYAJrdt23bAQVABIMly1ZsoCCMgUWCIYCB6AJBhIRDAQNgBIMFEwlt2i1CEwmWrAJBgI7FtpGCHYuWKH5QxEwpQDlo7F0A7IqBZBEwo7BCIwCBJo53CJoxiCJpIAdgOmzVpAQR/CgAIEAQJ2CBAoCBBIMmCg1oD4QLGFQUCCjQ+CKYw+CKY4JCKYwoCGRMaGREJDoroCgwdFzBlLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJox/CgAA="))
buffer : require("heatshrink").decompress((atob("yVJkgCCyf/AAPJBAYCBk4JB8gUFyVP//yBAoCB//5BAwUCAAIUHAAIgGChopGv5TIn5TIz4yLKYxxC/iGI/xxGKH5Q/agwAnUP7y/KH4yGeVYAJ0hGF34JB6RGFr4JB9JkFl4JB+gdFy4JB/QdFpYJB/odFkp4CS4xGCWoyhCKH5QuDoxQCDpI7GDoJZGHYIRGLIQvGO4QvGMQRNJADv+GIqTC/5PGz4JBJ41JBIPJCg2TD4QLGn4JB/gUaHwRTGHwRTHBIRTGNAQyJ8gyI+QdFp4JB/IdFk5lLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJoyJC/4A=="))),
palette: colorPalette
};
var bgRightNotFullscreen = {
width : 27, height : 152, bpp : 3,
transparent : 0,
buffer : require("heatshrink").decompress(atob("lmy5YCDBIUyBAmy5AJBhYUG2EAhgIFAQMAgQIGCgQABCg4ABEAwUNFI2AKZHAKZEgGRZTGOIUDQxJxGKH5Q/agwAxrdt23bAQVABIMly1ZsoCCMgUWCIYCB6AJBhIRDAQNgBIMFEwlt2i1CEwmWrAJBgI7FtpGCHYuWKH5QxEwpQDlo7F0A7IqBZBEwo7BCIwCBJo53CJoxiCJpIAdgOmzVpAQR/CgAIEAQJ2CBAoCBBIMmCg1oD4QLGFQUCCjQ+CKYw+CKY4JCKYwoCGRMaGREJDoroCgwdFzBlLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJox/CgA="))
buffer : require("heatshrink").decompress((atob("yVJkgCCyf/AAPJBAYCBk4JB8gUFyVP//yBAoCB//5BAwUCAAIUHAAIgGChopGv5TIn5TIz4yLKYxxC/iGI/xxGKH5Q/agwAx0hGF34JB6RGFr4JB9JkFl4JB+gdFy4JB/QdFpYJB/odFkqrCS4xGCWoyhCKH5QuDoxQCDpI7GDoJZGHYIRGLIQvGO4QvGMQRNJADv+GIqTC/5PGz4JBJ41JBIPJCg2TD4QLGn4JB/gUaHwRTGHwRTHBIRTGNAQyJ8gyI+QdFp4JB/IdFk5lLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJoyJC/4A="))),
palette: colorPalette
};
var bgLeft = settings.fullscreen ? bgLeftFullscreen : bgLeftNotFullscreen;
@ -191,7 +238,7 @@ function _drawData(key, y, c){
value = E.getAnalogVRef().toFixed(2) + "V";
} else if(key == "HRM"){
value = Math.round(Bangle.getHealthStatus("day").bpm);
value = Math.round(Bangle.getHealthStatus("last").bpm);
} else if (key == "TEMP"){
var weather = getWeather();
@ -244,9 +291,11 @@ function drawInfo(){
return;
}
// Draw Infor is called from different sources so
// we have to ensure that the alignment is always the same.
g.setFontAlign(-1, -1, 0);
g.setFontAntonioMedium();
g.setColor(cOrange);
g.setColor(color2);
g.clearRect(120, 10, g.getWidth(), 75);
g.drawString("LCARS", 128, 13);
@ -256,7 +305,7 @@ function drawInfo(){
g.drawString("NOCON", 128, 33);
}
if(Bangle.isLocked()){
g.setColor(cPurple);
g.setColor(color3);
g.drawString("LOCK", 128, 53);
}
}
@ -287,7 +336,7 @@ function drawState(){
g.drawString("STATUS", 23+26, 108);
} else {
// Alarm within symbol
g.setColor(cOrange);
g.setColor(color2);
g.drawString("ALARM", 23+26, 108);
g.setColor(cWhite);
g.setFontAntonioLarge();
@ -302,19 +351,19 @@ function drawPosition0(){
// Draw background image
var offset = settings.fullscreen ? 0 : 24;
g.drawImage(bgLeft, 0, offset);
drawHorizontalBgLine(cBlue, 25, 120, offset, 4);
drawHorizontalBgLine(cBlue, 130, 176, offset, 4);
drawHorizontalBgLine(cPurple, 20, 70, 80, 4);
drawHorizontalBgLine(cPurple, 80, 176, 80, 4);
drawHorizontalBgLine(cOrange, 35, 110, 87, 4);
drawHorizontalBgLine(cOrange, 120, 176, 87, 4);
drawHorizontalBgLine(color1, 25, 120, offset, 4);
drawHorizontalBgLine(color1, 130, 176, offset, 4);
drawHorizontalBgLine(color3, 20, 70, 80, 4);
drawHorizontalBgLine(color3, 80, 176, 80, 4);
drawHorizontalBgLine(color2, 35, 110, 87, 4);
drawHorizontalBgLine(color2, 120, 176, 87, 4);
// The last line is a battery indicator too
var bat = E.getBattery() / 100.0;
var batStart = 19;
var batWidth = 172 - batStart;
var batX2 = parseInt(batWidth * bat + batStart);
drawHorizontalBgLine(cOrange, batStart, batX2, 171, 5);
drawHorizontalBgLine(color2, batStart, batX2, 171, 5);
drawHorizontalBgLine(cGrey, batX2, 172, 171, 5);
for(var i=0; i+batStart<=172; i+=parseInt(batWidth/4)){
drawHorizontalBgLine(cBlack, batStart+i, batStart+i+3, 168, 8)
@ -353,9 +402,9 @@ function drawPosition0(){
// Draw data
g.setFontAlign(-1, -1, 0);
g.setColor(cWhite);
drawData(settings.dataRow1, 97, cOrange);
drawData(settings.dataRow2, 122, cPurple);
drawData(settings.dataRow3, 147, cBlue);
drawData(settings.dataRow1, 97, color2);
drawData(settings.dataRow2, 122, color3);
drawData(settings.dataRow3, 147, color1);
// Draw state
drawState();
@ -366,13 +415,13 @@ function drawPosition1(){
var offset = settings.fullscreen ? 0 : 24;
g.drawImage(bgRight, 149, offset);
if(settings.fullscreen){
drawHorizontalBgLine(cBlue, 0, 140, offset, 4);
drawHorizontalBgLine(color1, 0, 140, offset, 4);
}
drawHorizontalBgLine(cPurple, 0, 80, 80, 4);
drawHorizontalBgLine(cPurple, 90, 150, 80, 4);
drawHorizontalBgLine(cOrange, 0, 50, 87, 4);
drawHorizontalBgLine(cOrange, 60, 140, 87, 4);
drawHorizontalBgLine(cOrange, 0, 150, 171, 5);
drawHorizontalBgLine(color3, 0, 80, 80, 4);
drawHorizontalBgLine(color3, 90, 150, 80, 4);
drawHorizontalBgLine(color2, 0, 50, 87, 4);
drawHorizontalBgLine(color2, 60, 140, 87, 4);
drawHorizontalBgLine(color2, 0, 150, 171, 5);
// Draw steps bars
g.setColor(cWhite);
@ -511,17 +560,20 @@ function draw(){
* Step counter via widget
*/
function getSteps() {
var steps = 0;
try{
if (WIDGETS.wpedom !== undefined) {
return WIDGETS.wpedom.getSteps();
steps = WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
return WIDGETS.activepedom.getSteps();
steps = WIDGETS.activepedom.getSteps();
} else {
steps = Bangle.getHealthStatus("day").steps;
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
}
return 0;
return steps;
}
@ -530,21 +582,6 @@ function getWeather(){
try {
weatherJson = storage.readJSON('weather.json');
} catch(ex) {
// Return default
}
if(weatherJson === undefined){
return {
temp: "-",
hum: "-",
txt: "-",
wind: "-",
wdir: "-",
wrose: "-"
};
}
var weather = weatherJson.weather;
// Temperature
@ -559,8 +596,20 @@ function getWeather(){
weather.wind = Math.round(wind[1] * speedFactor);
return weather
}
} catch(ex) {
// Return default
}
return {
temp: " ? ",
hum: " ? ",
txt: " ? ",
wind: " ? ",
wdir: " ? ",
wrose: " ? "
};
}
/*
* Handle alarm
@ -642,7 +691,6 @@ Bangle.on('charging',function(charging) {
drawState();
});
function feedback(){
Bangle.buzz(40, 0.3);
}

View File

@ -5,11 +5,14 @@
const storage = require('Storage')
let settings = {
alarm: -1,
dataRow1: "Battery",
dataRow2: "Steps",
dataRow3: "Temp",
dataRow1: "Steps",
dataRow2: "HRM",
dataRow3: "Battery",
speed: "kph",
fullscreen: false,
themeColor1BG: "#FF9900",
themeColor2BG: "#FF00DC",
themeColor3BG: "#0094FF",
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@ -20,8 +23,11 @@
storage.write(SETTINGS_FILE, settings)
}
var dataOptions = ["Steps", "Battery", "VREF", "HRM", "Temp", "Humidity", "Wind", "Altitude", "CoreT"];
var speedOptions = ["kph", "mph"];
var color_options = ['Green','Orange','Cyan','Purple','Red','Blue','Yellow','White'];
var bg_code = ['#00ff00','#FF9900','#0094FF','#FF00DC','#ff0000','#0000ff','#ffef00','#FFFFFF'];
E.showMenu({
'': { 'title': 'LCARS Clock' },
@ -69,6 +75,33 @@
settings.speed = speedOptions[v];
save();
},
},
'Theme Color 1': {
value: 0 | bg_code.indexOf(settings.themeColor1BG),
min: 0, max: 7,
format: v => color_options[v],
onchange: v => {
settings.themeColor1BG = bg_code[v];
save();
},
},
'Theme Color 2': {
value: 0 | bg_code.indexOf(settings.themeColor2BG),
min: 0, max: 7,
format: v => color_options[v],
onchange: v => {
settings.themeColor2BG = bg_code[v];
save();
},
},
'Theme Color 3': {
value: 0 | bg_code.indexOf(settings.themeColor3BG),
min: 0, max: 7,
format: v => color_options[v],
onchange: v => {
settings.themeColor3BG = bg_code[v];
save();
},
}
});
})

View File

@ -3,7 +3,7 @@
"name": "LCARS Clock",
"shortName":"LCARS",
"icon": "lcars.png",
"version":"0.20",
"version":"0.22",
"readme": "README.md",
"supports": ["BANGLEJS2"],
"description": "Library Computer Access Retrieval System (LCARS) clock.",

View File

@ -583,6 +583,24 @@ var locales = {
abday: "ne,po,út,st,čt,pá,so",
day: "neděle,pondělí,úterý,středa,čtvrtek,pátek,sobota",
trans: { yes: "ano", Yes: "Ano", no: "ne", No: "Ne", ok: "ok", on: "zap", off: "vyp" }
},
"hr_HR": {
lang: "hr_HR",
decimal_point: ",",
thousands_sep: ".",
currency_symbol: "€",
int_curr_symbol: "EUR",
speed: "km/h",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "dop.", 1: "pop." },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%-d. %b %Y", 1: "%-d.%-m.%Y" }, // "3. jan. 2020" // "3.1.2020"(short)
abmonth: "sij.,velj.,ožu.,tra.,svi,lip.,srp.,kol.,ruj.,lis.,stu.,pro.",
month: "siječanj,veljača,ožujak,travanj,svibanj,lipanj,srpanj,kolovoz,rujan,listopad,studeni,prosinac",
abday: "ned.,pon.,uto.,sri.,čet.,pet.,sub.",
day: "nedjelja,ponedjeljak,utorak,srijeda,četvrtak,petak,subota",
trans: { yes: "da", Yes: "Da", no: "ne", No: "Ne", ok: "ok", on: "Uklj.", off: "Isklj.", "< Back": "< Natrag" }
},
"sl_SI": {
lang: "sl_SI",
@ -662,7 +680,7 @@ var locales = {
thousands_sep: " ",
currency_symbol: "kr",
int_curr_symbol: "NOK",
speed: "kmh",
speed: "kmt",
distance: { 0: "m", 1: "km" },
temperature: "°C",
ampm: { 0: "", 1: "" },

View File

@ -20,6 +20,6 @@ const avg = [];
setInterval(function() {
drawTemperature();
}, 2000);
E.showMessage("Loading...");
E.showMessage(/*LANG*/"Loading...");
Bangle.loadWidgets();
Bangle.drawWidgets();

View File

@ -44,3 +44,6 @@
0.29: Fix message list overwrites on Bangle.js 1 (fix #1642)
0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel)
0.31: Option to disable icon flashing
0.32: Added an option to allow quiet mode to override message auto-open
0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going
0.34: Don't buzz for 'map' update messages

View File

@ -52,7 +52,8 @@ var MESSAGES = require("Storage").readJSON("messages.json",1)||[];
if (!Array.isArray(MESSAGES)) MESSAGES=[];
var onMessagesModified = function(msg) {
// TODO: if new, show this new one
if (msg && msg.id!=="music" && msg.new && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
if (msg && msg.id!=="music" && msg.new && active!="map" &&
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
if (WIDGETS["messages"]) WIDGETS["messages"].buzz();
else Bangle.buzz();
}
@ -470,8 +471,6 @@ function checkMessages(options) {
// no new messages - go to clock?
if (options.clockIfAllRead && newMessages.length==0)
return load();
// we don't have to time out of this screen...
cancelReloadTimeout();
active = "main";
// Otherwise show a menu
E.showScroller({

View File

@ -56,9 +56,16 @@ exports.pushMessage = function(event) {
}
// otherwise load messages/show widget
var loadMessages = Bangle.CLOCK || event.important;
// first, buzz
var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet;
var unlockWatch = (require('Storage').readJSON('messages.settings.json',1)||{}).unlockWatch;
var appSettings = require('Storage').readJSON('messages.settings.json',1)||{};
var unlockWatch = appSettings.unlockWatch;
var quietNoAutOpn = appSettings.quietNoAutOpn;
delete appSettings;
// don't auto-open messages in quiet mode if quietNoAutOpn is true
if(quiet && quietNoAutOpn) {
loadMessages = false;
}
// first, buzz
if (!quiet && loadMessages && global.WIDGETS && WIDGETS.messages){
WIDGETS.messages.buzz();
if(unlockWatch != false){

View File

@ -1,7 +1,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.31",
"version": "0.34",
"description": "App to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -53,6 +53,11 @@
format: v => v?/*LANG*/'Yes':/*LANG*/'No',
onchange: v => updateSetting("flash", v)
},
/*LANG*/'Quiet mode disables auto-open': {
value: !!settings().quietNoAutOpn,
format: v => v?/*LANG*/'Yes':/*LANG*/'No',
onchange: v => updateSetting("quietNoAutOpn", v)
},
};
E.showMenu(mainmenu);
})

View File

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

View File

@ -0,0 +1,15 @@
Hacky app that uses Messages app and it's library to push a message that triggers the music controls. It's nearly not an app, and yet it moves.
This app require Messages setting 'Auto-open Music' to be 'Yes'. If it isn't, the app will change it to 'Yes' and let it stay that way.
Making the music controls accessible this way lets one start a music stream on the phone in some situations even though the message app didn't receive a music message from gadgetbridge to begin with. (I think.)
It is suggested to use Messages Music along side the app Quick Launch.
Messages Music v0.01 has been verified to work with Messages v0.31 on Bangle.js 2 fw2v13.
Music Messages should work with forks of the original Messages app. At least as long as functions pushMessage() in the library and showMusicMessage() in app.js hasn't been changed too much.
Messages app is created by Gordon Williams with contributions from [Jeroen Peters](https://github.com/jeroenpeters1986).
The icon used for this app is from [https://icons8.com](https://icons8.com).

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AFXdAAQVVDKQWHDB0NC5PQCyoYMCxZJKFxgwKCxowJC6xGOJBALE6YwRBQnf+YXPIwvf/4YKJAgXHDBQXNDBIXO/89C5YKFC4gYIC54YHC6AYGC6IYFC9JHWO6ynLa64XJ+YWGC5wWIC5oWJC4p4F74WKOwgXG6YWKC4xIFABRGFYI4uPC7JIOIw4wPCxAwNFxIYMCxZJLCxgYJCxwZGCqIA/AC4="))

15
apps/messagesmusic/app.js Normal file
View File

@ -0,0 +1,15 @@
let showMusic = () => {
Bangle.CLOCK = 1; // To pass condition in messages library
require('messages').pushMessage({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true});
Bangle.CLOCK = undefined;
};
var settings = require('Storage').readJSON('messages.settings.json', true) || {}; //read settings if they exist else set to empty dict
if (!settings.openMusic) {
settings.openMusic = true; // This app/hack works as intended only if this setting is true
require('Storage').writeJSON('messages.settings.json', settings);
E.showMessage("First run:\n\nMessages setting\n\n 'Auto-Open Music'\n\n set to 'Yes'");
setTimeout(()=>{showMusic();}, 5000);
} else {
showMusic();
}

BIN
apps/messagesmusic/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,18 @@
{
"id": "messagesmusic",
"name":"Messages Music",
"version":"0.01",
"description": "Uses Messages library to push a music message which in turn displays Messages app music controls",
"icon":"app.png",
"type": "app",
"tags":"tool,music",
"screenshots": [{"url":"screenshot.png"}],
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"messagesmusic.app.js","url":"app.js"},
{"name":"messagesmusic.img","url":"app-icon.js","evaluate":true}
],
"dependencies": {"messages":"app"}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

1
apps/mosaic/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: First release

14
apps/mosaic/README.md Normal file
View File

@ -0,0 +1,14 @@
# Mosaic Clock
A fabulously colourful clock!
* Clearly shows the time on a colourful background that changes every minute.
* Dark and Light theme compatible, with a setting to override the digit colour scheme.
* Show or hide widgets with a setting (default shows widgets).
![](mosaic-scr1.png)
![](mosaic-scr2.png)
This clock is inspired by the mosaic watchface for pebble: https://apps.rebble.io/en_US/application/55386bcd2aead62b16000028
Written by: [Sir Indy](https://github.com/sir-indy) For support and discussion please post in the [Bangle JS Forum](http://forum.espruino.com/microcosms/1424/)

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

@ -0,0 +1,19 @@
{
"id":"mosaic",
"name":"Mosaic Clock",
"shortName": "Mosaic Clock",
"version": "0.01",
"description": "A fabulously colourful clock",
"readme": "README.md",
"icon":"mosaic.png",
"screenshots": [{"url":"mosaic-scr1.png"},{"url":"mosaic-scr2.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS", "BANGLEJS2"],
"allow_emulator": true,
"storage": [
{"name":"mosaic.app.js","url":"mosaic.app.js"},
{"name":"mosaic.settings.js","url":"mosaic.settings.js"},
{"name":"mosaic.img","url":"mosaic.icon.js","evaluate":true}
]
}

BIN
apps/mosaic/mosaic-scr1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
apps/mosaic/mosaic-scr2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

103
apps/mosaic/mosaic.app.js Normal file
View File

@ -0,0 +1,103 @@
Array.prototype.sample = function(){
return this[Math.floor(Math.random()*this.length)];
};
const SETTINGS_FILE = "mosaic.settings.json";
let settings;
let theme;
let timeout = 60;
let drawTimeout;
let colours = [
'#f00', '#00f', '#0f0', '#ff0', '#f0f', '#0ff',
'#8f0', '#f08', '#f80', '#80f', '#0f8', '#08f',
];
let digits = [
E.toArrayBuffer(atob("BQcB/Gtax+A=")),
E.toArrayBuffer(atob("BQeCAX9c1zXNc1zX9A==")),
E.toArrayBuffer(atob("BQcB/Hsbx+A=")),
E.toArrayBuffer(atob("BQcB/Hsex+A=")),
E.toArrayBuffer(atob("BQeCAf/zPM8D/Nc1/A==")),
E.toArrayBuffer(atob("BQcB/G8ex+A=")),
E.toArrayBuffer(atob("BQcB/G8ax+A=")),
E.toArrayBuffer(atob("BQeCAf/wP81zXNc1/A==")),
E.toArrayBuffer(atob("BQcB/Gsax+A=")),
E.toArrayBuffer(atob("BQcB/Gsex+A="))
];
function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'showWidgets': false, 'theme':'System'};
}
function loadThemeColors() {
theme = {fg: g.theme.fg, bg: g.theme.bg};
if (settings.theme === "Dark") {
theme.fg = g.toColor(1,1,1);
theme.bg = g.toColor(0,0,0);
}
else if (settings.theme === "Light") {
theme.fg = g.toColor(0,0,0);
theme.bg = g.toColor(1,1,1);
}
}
function queueDraw(seconds) {
let millisecs = seconds * 1000;
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, millisecs - (Date.now() % millisecs));
}
function draw() {
// draw colourful grid
for (let i_x = 0; i_x < num_squares_w; i_x++) {
for (let i_y = 0; i_y < num_squares_h; i_y++) {
g.setColor(colours.sample()).fillRect(
o_w+i_x*s, o_h+i_y*s, o_w+i_x*s+s, o_h+i_y*s+s
);
}
}
let t = new Date();
g.setBgColor(theme.fg);
g.setColor(theme.bg);
g.drawImage(digits[Math.floor(t.getHours()/10)], (mid_x-5)*s+o_w, (mid_y-7)*s+o_h, {scale:s});
g.drawImage(digits[t.getHours() % 10], (mid_x+1)*s+o_w, (mid_y-7)*s+o_h, {scale:s});
g.drawImage(digits[Math.floor(t.getMinutes()/10)], (mid_x-5)*s+o_w, (mid_y+1)*s+o_h, {scale:s});
g.drawImage(digits[t.getMinutes() % 10], (mid_x+1)*s+o_w, (mid_y+1)*s+o_h, {scale:s});
queueDraw(timeout);
}
g.clear();
loadSettings();
loadThemeColors();
offset_widgets = settings.showWidgets ? 24 : 0;
let available_height = g.getHeight() - offset_widgets;
// Calculate grid size and offsets
let s = Math.floor(available_height/17);
let num_squares_w = Math.round(g.getWidth()/s) - 1;
let num_squares_h = Math.round(available_height/s) - 1;
let o_w = Math.floor((g.getWidth() - num_squares_w * s)/2);
let o_h = Math.floor((g.getHeight() - num_squares_h * s+offset_widgets)/2);
let mid_x = Math.floor(num_squares_w/2);
let mid_y = Math.floor((num_squares_h-1)/2);
draw();
Bangle.on('lcdPower',on=>{
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
});
Bangle.setUI('clock');
if (settings.showWidgets) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcAtu27dt3MkyVJkgHC23UA4WSCP4R/CP4RFBAfSA4VJA4QCFCP4R/CP4RJ7oaMCP4R/CP4RFbge9BoYID3IQCkgR/CP4R/CIoA=="))

BIN
apps/mosaic/mosaic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

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