Merge branch 'espruino:master' into tapkb2
|
@ -10,3 +10,4 @@ _config.yml
|
|||
tests/Layout/bin/tmp.*
|
||||
tests/Layout/testresult.bmp
|
||||
apps.local.json
|
||||
_site
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New app!
|
||||
0.02: Better support for watch themes
|
|
@ -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,
|
||||
|
|
|
@ -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;}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwYda7dtwAQNmwRB2wQMgO2CIXACJcNCIfYCJYOCCgQRNJQYRM2ADBgwR/CKprRWAKPQWZ0DCIjXLjYREGpYODAQVgCBB3Btj+EAoQAGO4IdCgImDCAwLCAoo4IF4J3DCIPDCIQ4FO4VtwARCAoIRGRgQCBa4IRCKAQRERgOwIIIRDAoOACIoIBwwRHLIqMCFgIRCGQQRIWAYRLYQoREWwTmHO4IRCFgLXHPoi/CbogAFEAIRCWwTpKEwZBCHwK5BCJZEBCJZcCGQTLDCJK/BAQIRKMoaSDOIYAFeQYRMcYRWBXIUAWYPACIq8DagfACJQLCCIYsBU4QRF7B9CAogRGI4QLCAoprIMoZKER5C/DAoShMAo4AGfAQFIACQ="))
|
|
@ -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();
|
After Width: | Height: | Size: 10 KiB |
|
@ -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);
|
||||
}
|
||||
|
|
@ -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") || [];
|
||||
};
|
|
@ -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"}
|
||||
]
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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.
|
|
@ -1,13 +1,13 @@
|
|||
# Black & White clock
|
||||
# BW Clock
|
||||
|
||||

|
||||
|
||||
## 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>
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"],
|
||||
|
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.2 KiB |
|
@ -18,7 +18,7 @@
|
|||
|
||||
|
||||
E.showMenu({
|
||||
'': { 'title': 'BlackWhite Clock' },
|
||||
'': { 'title': 'BW Clock' },
|
||||
'< Back': back,
|
||||
'Fullscreen': {
|
||||
value: settings.fullscreen,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -244,7 +244,7 @@ function run(){
|
|||
Bangle.setLCDMode();
|
||||
g.clear();
|
||||
g.flip();
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
load(app.src);
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App! Very limited course support.
|
|
@ -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.
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## 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)
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("lEowcF23btoCRpMkyVKBxHayQOCpNbBw/UBwkrBw21BYQCCkoOIy/JCIetI4+X+QgEBxNIBxFqBxFWBxAsEBxHpBYPbsgOFqQOEy3btgOLDoIOrrYOKMoQOKOgYsKBx1KBwaGBWYgOBkVJBwKGBYQwOBiVJlIdCkmVBxdZfwwOGF4IONkoLCB2J3BBxkgQwQOKWYlJlYOUtQORtskzQOHtoOE7QOLAQdbBw21BydKBYgCD6gODBYwCNA"))
|
|
@ -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();
|
After Width: | Height: | Size: 9.8 KiB |
|
@ -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();
|
||||
}
|
|
@ -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}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 3.3 KiB |
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
1.0: Initial release.
|
|
@ -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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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):
|
||||
|
||||

|
||||
|
||||
When you're done, simply press the side button to exit the app.
|
||||
|
||||
## Creator
|
||||
|
||||
[Uberi](https://github.com/Uberi)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkBiIA/AHqFCAxQWJ5gABCIQGGABEQB4QABgIGGC5MMCAnAAwwuOABAwIC64/FABBIIC68ADBnAVJEP+AXLBoJ2H/4XN/54GBAIXOGAouBBAMAABQXBGAoHCAB4wDFwQARGAYvWL7CPDbBXAR46/DiAXJgK/Id4URGBHABobwHEAIwIBQQuHAAcYGA3AwIUKC4eAC4sIC5+IGAnAxAXQkAXDgQXRkQwC4EiC6QwCgQXTl0M4HiC6nghwXV93uC9MRC44WOGAIXFFx4ABC4oWQiMSC4chC6MRC4YWSiMeC4PhC6cRC4IWUGAIuVAH4AVA="))
|
|
@ -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");
|
After Width: | Height: | Size: 4.8 KiB |
|
@ -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"}]
|
||||
}
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
|
@ -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; }},
|
||||
});
|
||||
})
|
|
@ -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",
|
||||
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Now keeps user input trace intact by changing how the screen is updated.
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`.
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
|
@ -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)
|
||||
|
|
Before Width: | Height: | Size: 795 B After Width: | Height: | Size: 789 B |
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 760 B |
Before Width: | Height: | Size: 791 B After Width: | Height: | Size: 771 B |
Before Width: | Height: | Size: 769 B After Width: | Height: | Size: 742 B |
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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: "" },
|
||||
|
|
|
@ -20,6 +20,6 @@ const avg = [];
|
|||
setInterval(function() {
|
||||
drawTemperature();
|
||||
}, 2000);
|
||||
E.showMessage("Loading...");
|
||||
E.showMessage(/*LANG*/"Loading...");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -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).
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AFXdAAQVVDKQWHDB0NC5PQCyoYMCxZJKFxgwKCxowJC6xGOJBALE6YwRBQnf+YXPIwvf/4YKJAgXHDBQXNDBIXO/89C5YKFC4gYIC54YHC6AYGC6IYFC9JHWO6ynLa64XJ+YWGC5wWIC5oWJC4p4F74WKOwgXG6YWKC4xIFABRGFYI4uPC7JIOIw4wPCxAwNFxIYMCxZJLCxgYJCxwZGCqIA/AC4="))
|
|
@ -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();
|
||||
}
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -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"}
|
||||
|
||||
}
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1 @@
|
|||
0.01: First release
|
|
@ -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).
|
||||
|
||||

|
||||

|
||||
|
||||
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/)
|
|
@ -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}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 28 KiB |
|
@ -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();
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwcAtu27dt3MkyVJkgHC23UA4WSCP4R/CP4RFBAfSA4VJA4QCFCP4R/CP4RJ7oaMCP4R/CP4RFbge9BoYID3IQCkgR/CP4R/CIoA=="))
|
After Width: | Height: | Size: 649 B |