185
apps.json
|
@ -78,7 +78,7 @@
|
|||
{ "id": "welcome",
|
||||
"name": "Welcome",
|
||||
"icon": "app.png",
|
||||
"version":"0.07",
|
||||
"version":"0.08",
|
||||
"description": "Appears at first boot and explains how to use Bangle.js",
|
||||
"tags": "start,welcome",
|
||||
"allow_emulator":true,
|
||||
|
@ -86,8 +86,10 @@
|
|||
{"name":"welcome.boot.js","url":"boot.js"},
|
||||
{"name":"welcome.app.js","url":"app.js"},
|
||||
{"name":"welcome.settings.js","url":"settings.js"},
|
||||
{"name":"welcome.settings.json","url":"settings-default.json","evaluate":true},
|
||||
{"name":"welcome.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{"name":"welcome.json"}
|
||||
]
|
||||
},
|
||||
{ "id": "gbridge",
|
||||
|
@ -106,7 +108,7 @@
|
|||
{ "id": "mclock",
|
||||
"name": "Morphing Clock",
|
||||
"icon": "clock-morphing.png",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "7 segment clock that morphs between minutes and hours",
|
||||
"tags": "clock",
|
||||
"type":"clock",
|
||||
|
@ -120,13 +122,12 @@
|
|||
{ "id": "setting",
|
||||
"name": "Settings",
|
||||
"icon": "settings.png",
|
||||
"version":"0.17",
|
||||
"version":"0.18",
|
||||
"description": "A menu for setting up Bangle.js",
|
||||
"tags": "tool,system",
|
||||
"storage": [
|
||||
{"name":"setting.app.js","url":"settings.js"},
|
||||
{"name":"setting.boot.js","url":"boot.js"},
|
||||
{"name":"setting.json","url":"settings-default.json","evaluate":true},
|
||||
{"name":"setting.img","url":"settings-icon.js","evaluate":true}
|
||||
],
|
||||
"sortorder" : -2
|
||||
|
@ -135,16 +136,18 @@
|
|||
"name": "Default Alarm",
|
||||
"shortName":"Alarms",
|
||||
"icon": "app.png",
|
||||
"version":"0.06",
|
||||
"version":"0.07",
|
||||
"description": "Set and respond to alarms",
|
||||
"tags": "tool,alarm,widget",
|
||||
"storage": [
|
||||
{"name":"alarm.app.js","url":"app.js"},
|
||||
{"name":"alarm.boot.js","url":"boot.js"},
|
||||
{"name":"alarm.js","url":"alarm.js"},
|
||||
{"name":"alarm.json","content":"[]"},
|
||||
{"name":"alarm.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"alarm.wid.js","url":"widget.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"alarm.json"}
|
||||
]
|
||||
},
|
||||
{ "id": "wclock",
|
||||
|
@ -280,15 +283,18 @@
|
|||
{ "id": "gpsrec",
|
||||
"name": "GPS Recorder",
|
||||
"icon": "app.png",
|
||||
"version":"0.07",
|
||||
"version":"0.08",
|
||||
"interface": "interface.html",
|
||||
"description": "Application that allows you to record a GPS track. Can run in background",
|
||||
"tags": "tool,outdoors,gps,widget",
|
||||
"storage": [
|
||||
{"name":"gpsrec.app.js","url":"app.js"},
|
||||
{"name":"gpsrec.json","url":"app-settings.json","evaluate":true},
|
||||
{"name":"gpsrec.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"gpsrec.wid.js","url":"widget.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"gpsrec.json"},
|
||||
{"wildcard":".gpsrc?","storageFile": true}
|
||||
]
|
||||
},
|
||||
{ "id": "gpsnav",
|
||||
|
@ -297,6 +303,7 @@
|
|||
"version":"0.01",
|
||||
"description": "Displays GPS Course and Speed, + Directions to waypoint and waypoint recording",
|
||||
"tags": "tool,outdoors,gps",
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"gpsnav.app.js","url":"app.js"},
|
||||
{"name":"waypoints.json","url":"waypoints.json","evaluate":false},
|
||||
|
@ -306,15 +313,18 @@
|
|||
{ "id": "heart",
|
||||
"name": "Heart Rate Recorder",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"version":"0.02",
|
||||
"interface": "interface.html",
|
||||
"description": "Application that allows you to record your heart rate. Can run in background",
|
||||
"tags": "tool,health,widget",
|
||||
"storage": [
|
||||
{"name":"heart.app.js","url":"app.js"},
|
||||
{"name":"heart.json","url":"app-settings.json","evaluate":true},
|
||||
{"name":"heart.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"heart.wid.js","url":"widget.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"heart.json"},
|
||||
{"wildcard":".heart?","storageFile": true}
|
||||
]
|
||||
},
|
||||
{ "id": "slevel",
|
||||
|
@ -339,6 +349,23 @@
|
|||
{"name":"files.img","url":"files-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "weather",
|
||||
"name": "Weather",
|
||||
"icon": "icon.png",
|
||||
"version":"0.01",
|
||||
"description": "Show Gadgetbridge weather report",
|
||||
"readme": "readme.md",
|
||||
"tags": "widget,outdoors",
|
||||
"storage": [
|
||||
{"name":"weather.app.js","url":"app.js"},
|
||||
{"name":"weather.wid.js","url":"widget.js"},
|
||||
{"name":"weather","url":"lib.js"},
|
||||
{"name":"weather.img","url":"icon.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{"name": "weather.json"}
|
||||
]
|
||||
},
|
||||
{ "id": "widbat",
|
||||
"name": "Battery Level Widget",
|
||||
"icon": "widget.png",
|
||||
|
@ -354,14 +381,16 @@
|
|||
"name": "Battery Level Widget (with percentage)",
|
||||
"shortName": "Battery Widget",
|
||||
"icon": "widget.png",
|
||||
"version":"0.09",
|
||||
"version":"0.11",
|
||||
"description": "Show the current battery level and charging status in the top right of the clock, with charge percentage",
|
||||
"tags": "widget,battery",
|
||||
"type":"widget",
|
||||
"storage": [
|
||||
{"name":"widbatpc.wid.js","url":"widget.js"},
|
||||
{"name":"widbatpc.settings.js","url":"settings.js"},
|
||||
{"name":"widbatpc.settings.json","content": "{}"}
|
||||
{"name":"widbatpc.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name":"widbatpc.json"}
|
||||
]
|
||||
},
|
||||
{ "id": "widbt",
|
||||
|
@ -529,20 +558,22 @@
|
|||
"id": "ncstart",
|
||||
"name": "NCEU Startup",
|
||||
"icon": "start.png",
|
||||
"version":"0.04",
|
||||
"version":"0.05",
|
||||
"description": "NodeConfEU 2019 'First Start' Sequence",
|
||||
"tags": "start,welcome",
|
||||
"storage": [
|
||||
{"name":"ncstart.app.js","url":"start.js"},
|
||||
{"name":"ncstart.boot.js","url":"boot.js"},
|
||||
{"name":"ncstart.settings.js","url":"settings.js"},
|
||||
{"name":"ncstart.settings.json","url":"settings-default.json","evaluate":true},
|
||||
{"name":"ncstart.img","url":"start-icon.js","evaluate":true},
|
||||
{"name":"nc-bangle.img","url":"start-bangle.js","evaluate":true},
|
||||
{"name":"nc-nceu.img","url":"start-nceu.js","evaluate":true},
|
||||
{"name":"nc-nfr.img","url":"start-nfr.js","evaluate":true},
|
||||
{"name":"nc-nodew.img","url":"start-nodew.js","evaluate":true},
|
||||
{"name":"nc-tf.img","url":"start-tf.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{"name":"ncstart.json"}
|
||||
]
|
||||
},
|
||||
{ "id": "ncfrun",
|
||||
|
@ -1113,7 +1144,7 @@
|
|||
{ "id": "minionclk",
|
||||
"name": "Minion clock",
|
||||
"icon": "minionclk.png",
|
||||
"version": "0.01",
|
||||
"version": "0.02",
|
||||
"description": "Minion themed clock.",
|
||||
"tags": "clock,minion",
|
||||
"type": "clock",
|
||||
|
@ -1140,22 +1171,22 @@
|
|||
"name": "Active Pedometer",
|
||||
"shortName":"Active Pedometer",
|
||||
"icon": "app.png",
|
||||
"version":"0.02",
|
||||
"description": "Pedometer that filters out arm movement and displays a step goal progress.",
|
||||
"version":"0.03",
|
||||
"description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.",
|
||||
"tags": "outdoors,widget",
|
||||
"type":"widget",
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"activepedom.wid.js","url":"widget.js"},
|
||||
{"name":"activepedom.settings.js","url":"settings.js"},
|
||||
{"name":"activepedom.img","url":"app-icon.js","evaluate":true}
|
||||
{"name":"activepedom.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"activepedom.app.js","url":"app.js"}
|
||||
]
|
||||
},
|
||||
{ "id": "chronowid",
|
||||
"name": "Chrono Widget",
|
||||
"shortName":"Chrono Widget",
|
||||
"icon": "app.png",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"description": "Chronometer (timer) which runs as widget.",
|
||||
"tags": "tools,widget",
|
||||
"readme": "README.md",
|
||||
|
@ -1234,7 +1265,7 @@
|
|||
"name": "Numerals Clock",
|
||||
"shortName": "Numerals Clock",
|
||||
"icon": "numerals.png",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "A simple big numerals clock",
|
||||
"tags": "numerals,clock",
|
||||
"type":"clock",
|
||||
|
@ -1242,8 +1273,10 @@
|
|||
"storage": [
|
||||
{"name":"numerals.app.js","url":"numerals.app.js"},
|
||||
{"name":"numerals.img","url":"numerals-icon.js","evaluate":true},
|
||||
{"name":"numerals.settings.js","url":"numerals.settings.js"},
|
||||
{"name":"numerals.json","url":"numerals-default.json","evaluate":true}
|
||||
{"name":"numerals.settings.js","url":"numerals.settings.js"}
|
||||
],
|
||||
"data":[
|
||||
{"name":"numerals.json"}
|
||||
]
|
||||
},
|
||||
{ "id": "bledetect",
|
||||
|
@ -1306,6 +1339,68 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "buffgym",
|
||||
"name": "BuffGym",
|
||||
"icon": "buffgym.png",
|
||||
"version":"0.01",
|
||||
"description": "BuffGym is the famous 5x5 workout program for the BangleJS",
|
||||
"tags": "tool,outdoors,gym,exercise",
|
||||
"type": "app",
|
||||
"allow_emulator": false,
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"buffgym.app.js", "url": "buffgym.app.js"},
|
||||
{"name":"buffgym-set.js","url":"buffgym-set.js"},
|
||||
{"name":"buffgym-exercise.js","url":"buffgym-exercise.js"},
|
||||
{"name":"buffgym-program.js","url":"buffgym-program.js"},
|
||||
{"name":"buffgym-program-a.json","url":"buffgym-program-a.json"},
|
||||
{"name":"buffgym-program-b.json","url":"buffgym-program-b.json"},
|
||||
{"name":"buffgym-program-index.json","url":"buffgym-program-index.json"},
|
||||
{"name":"buffgym.img","url":"buffgym-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "banglerun",
|
||||
"name": "BangleRun",
|
||||
"shortName": "BangleRun",
|
||||
"icon": "banglerun.png",
|
||||
"version": "0.01",
|
||||
"description": "An app for running sessions.",
|
||||
"tags": "run,running,fitness,outdoors",
|
||||
"allow_emulator": false,
|
||||
"storage": [
|
||||
{
|
||||
"name": "banglerun.app.js",
|
||||
"url": "app.js"
|
||||
},
|
||||
{
|
||||
"name": "banglerun.img",
|
||||
"url": "app-icon.js",
|
||||
"evaluate": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "metronome",
|
||||
"name": "Metronome",
|
||||
"icon": "metronome_icon.png",
|
||||
"version": "0.03",
|
||||
"description": "Makes the watch blinking and vibrating with a given rate",
|
||||
"tags": "tool",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{
|
||||
"name": "metronome.app.js",
|
||||
"url": "metronome.js"
|
||||
},
|
||||
{
|
||||
"name": "metronome.img",
|
||||
"url": "metronome-icon.js",
|
||||
"evaluate": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "id": "blackjack",
|
||||
"name": "Black Jack game",
|
||||
"shortName":"Black Jack game",
|
||||
|
@ -1331,7 +1426,7 @@
|
|||
{"name":"hidcam.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"id": "rclock",
|
||||
"name": "Round clock with seconds, minutes and date",
|
||||
"shortName":"Round Clock",
|
||||
|
@ -1344,5 +1439,43 @@
|
|||
{"name":"rclock.app.js","url":"rclock.app.js"},
|
||||
{"name":"rclock.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "hamloc",
|
||||
"name": "QTH Locator / Maidenhead Locator System",
|
||||
"shortName": "QTH Locator",
|
||||
"icon": "app.png",
|
||||
"version":"0.01",
|
||||
"description": "Convert your current GPS location to the Maidenhead locator system used by HAM amateur radio operators",
|
||||
"tags": "tool,outdoors,gps",
|
||||
"storage": [
|
||||
{"name":"hamloc.app.js","url":"app.js"},
|
||||
{"name":"hamloc.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
},
|
||||
{ "id": "osmpoi",
|
||||
"name": "POI Compass",
|
||||
"icon": "app.png",
|
||||
"version":"0.02",
|
||||
"description": "Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.",
|
||||
"tags": "tool,outdoors,gps",
|
||||
"custom": "osmpoi.html",
|
||||
"storage": [
|
||||
{"name":"osmpoi.app.js"},
|
||||
{"name":"osmpoi.img"}
|
||||
]
|
||||
},
|
||||
{ "id": "pong",
|
||||
"name": "Pong",
|
||||
"shortName": "Pong",
|
||||
"icon": "pong.png",
|
||||
"version": "0.01",
|
||||
"description": "A clone of the Atari game Pong",
|
||||
"tags": "game",
|
||||
"type": "app",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"pong.app.js","url":"app.js"},
|
||||
{"name":"pong.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New Widget!
|
||||
0.02: Distance calculation and display
|
||||
0.02: Distance calculation and display
|
||||
0.03: Data logging and display
|
|
@ -1,4 +1,4 @@
|
|||
# Active Pedometer
|
||||
# Active Pedometer
|
||||
Pedometer that filters out arm movement and displays a step goal progress.
|
||||
|
||||
I changed the step counting algorithm completely.
|
||||
|
@ -6,6 +6,8 @@ Now every step is counted when in status 'active', if the time difference betwee
|
|||
To get in 'active' mode, you have to reach the step threshold before the active timer runs out.
|
||||
When you reach the step threshold, the steps needed to reach the threshold are counted as well.
|
||||
|
||||
Steps are saved to a datafile every 5 minutes. You can watch a graph using the app.
|
||||
|
||||
## Screenshots
|
||||
* 600 steps
|
||||

|
||||
|
@ -30,6 +32,22 @@ When you reach the step threshold, the steps needed to reach the threshold are c
|
|||
* Steps are saved to a file and read-in at start (to not lose step progress)
|
||||
* Settings can be changed in Settings - App/widget settings - Active Pedometer
|
||||
|
||||
## Data storage
|
||||
|
||||
* Data is stored to a file
|
||||
* Format: now,stepsCounted,active,stepsTooShort,stepsTooLong,stepsOutsideTime
|
||||
* now is UNIX timestamp in ms
|
||||
* You can chose the app to watch a steps graph
|
||||
* You can import the file into Excel
|
||||
* The file does not include a header
|
||||
* You can convert UNIX timestamp to a date in Excel using this formula: =DATUM(1970;1;1)+(LINKS(A2;10)/86400)
|
||||
* You have to format the cell with the formula to a date cell. Example: JJJJ-MM-TT-hh-mm-ss
|
||||
|
||||
## App
|
||||
|
||||
* The app accesses the data stored for the current day
|
||||
* Timespan is choseable (1h, 4h, 8h, 12h, 16h, 20, 24h), standard is 24h, the whole current day
|
||||
|
||||
## Settings
|
||||
|
||||
* Max time (ms): Maximum time between two steps in milliseconds, steps will not be counted if exceeded. Standard: 1100
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
(() => {
|
||||
|
||||
//Graph module, as long as modules are not added by the app loader
|
||||
Modules.addCached("graph",function(){exports.drawAxes=function(b,c,a){function h(a){return e+m*(a-t)/x}function l(a){return f+g-g*(a-n)/u}var k=a.padx||0,d=a.pady||0,t=-k,w=c.length+k-1,n=(void 0!==a.miny?a.miny:a.miny=c.reduce(function(a,b){return Math.min(a,b)},c[0]))-d;c=(void 0!==a.maxy?a.maxy:a.maxy=c.reduce(function(a,b){return Math.max(a,b)},c[0]))+d;a.gridy&&(d=a.gridy,n=d*Math.floor(n/d),c=d*Math.ceil(c/d));var e=a.x||0,f=a.y||0,m=a.width||b.getWidth()-(e+1),g=a.height||b.getHeight()-(f+1);a.axes&&(null!==a.ylabel&&
|
||||
(e+=6,m-=6),null!==a.xlabel&&(g-=6));a.title&&(f+=6,g-=6);a.axes&&(b.drawLine(e,f,e,f+g),b.drawLine(e,f+g,e+m,f+g));a.title&&(b.setFontAlign(0,-1),b.drawString(a.title,e+m/2,f-6));var x=w-t,u=c-n;u||(u=1);if(a.gridx){b.setFontAlign(0,-1,0);var v=a.gridx;for(d=Math.ceil((t+k)/v)*v;d<=w-k;d+=v){var r=h(d),p=a.xlabel?a.xlabel(d):d;b.setPixel(r,f+g-1);var q=b.stringWidth(p)/2;null!==a.xlabel&&r>q&&b.getWidth()>r+q&&b.drawString(p,r,f+g+2)}}if(a.gridy)for(b.setFontAlign(0,0,1),d=n;d<=c;d+=a.gridy)k=l(d),
|
||||
p=a.ylabel?a.ylabel(d):d,b.setPixel(e+1,k),q=b.stringWidth(p)/2,null!==a.ylabel&&k>q&&b.getHeight()>k+q&&b.drawString(p,e-5,k+1);b.setFontAlign(-1,-1,0);return{x:e,y:f,w:m,h:g,getx:h,gety:l}};exports.drawLine=function(b,c,a){a=a||{};a=exports.drawAxes(b,c,a);var h=!0,l;for(l in c)h?b.moveTo(a.getx(l),a.gety(c[l])):b.lineTo(a.getx(l),a.gety(c[l])),h=!1;return a};exports.drawBar=function(b,c,a){a=a||{};a.padx=1;a=exports.drawAxes(b,c,a);for(var h in c)b.fillRect(a.getx(h-.5)+1,a.gety(c[h]),a.getx(h+
|
||||
.5)-1,a.gety(0));return a}});
|
||||
|
||||
const storage = require("Storage");
|
||||
const SETTINGS_FILE = 'activepedom.settings.json';
|
||||
var history = 86400000; // 28800000=8h 43200000=12h //86400000=24h
|
||||
|
||||
//return setting
|
||||
function setting(key) {
|
||||
//define default settings
|
||||
const DEFAULTS = {
|
||||
'cMaxTime' : 1100,
|
||||
'cMinTime' : 240,
|
||||
'stepThreshold' : 30,
|
||||
'intervalResetActive' : 30000,
|
||||
'stepSensitivity' : 80,
|
||||
'stepGoal' : 10000,
|
||||
'stepLength' : 75,
|
||||
};
|
||||
if (!settings) { loadSettings(); }
|
||||
return (key in settings) ? settings[key] : DEFAULTS[key];
|
||||
}
|
||||
|
||||
//Convert ms to time
|
||||
function getTime(t) {
|
||||
date = new Date(t);
|
||||
offset = date.getTimezoneOffset() / 60;
|
||||
//var milliseconds = parseInt((t % 1000) / 100),
|
||||
seconds = Math.floor((t / 1000) % 60);
|
||||
minutes = Math.floor((t / (1000 * 60)) % 60);
|
||||
hours = Math.floor((t / (1000 * 60 * 60)) % 24);
|
||||
hours = hours - offset;
|
||||
hours = (hours < 10) ? "0" + hours : hours;
|
||||
minutes = (minutes < 10) ? "0" + minutes : minutes;
|
||||
seconds = (seconds < 10) ? "0" + seconds : seconds;
|
||||
return hours + ":" + minutes + ":" + seconds;
|
||||
}
|
||||
|
||||
function getDate(t) {
|
||||
date = new Date(t*1);
|
||||
year = date.getFullYear();
|
||||
month = date.getMonth()+1; //month is zero-based
|
||||
day = date.getDate();
|
||||
month = (month < 10) ? "0" + month : month;
|
||||
day = (day < 10) ? "0" + day : day;
|
||||
return year + "-" + month + "-" + day;
|
||||
}
|
||||
|
||||
//columns: 0=time, 1=stepsCounted, 2=active, 3=stepsTooShort, 4=stepsTooLong, 5=stepsOutsideTime
|
||||
function getArrayFromCSV(file, column) {
|
||||
i = 0;
|
||||
array = [];
|
||||
now = new Date();
|
||||
while ((nextLine = file.readLine())) { //as long as there is a next line
|
||||
if(nextLine) {
|
||||
dataSplitted = nextLine.split(','); //split line,
|
||||
diff = now - dataSplitted[0]; //calculate difference between now and stored time
|
||||
if (diff <= history) { //only entries from the last x ms
|
||||
array.push(dataSplitted[column]);
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function drawGraph() {
|
||||
//times
|
||||
// actives = getArrayFromCSV(csvFile, 2);
|
||||
// shorts = getArrayFromCSV(csvFile, 3);
|
||||
// longs = getArrayFromCSV(csvFile, 4);
|
||||
// outsides = getArrayFromCSV(csvFile, 5); //array.push(dataSplitted[5].slice(0,-1));
|
||||
now = new Date();
|
||||
month = now.getMonth() + 1;
|
||||
if (month < 10) month = "0" + month;
|
||||
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data";
|
||||
var csvFile = storage.open(filename, "r");
|
||||
times = getArrayFromCSV(csvFile, 0);
|
||||
first = getDate(times[0]) + " " + getTime(times[0]); //first entry in datafile
|
||||
last = getDate (times[times.length-1]) + " " + getTime(times[times.length-1]); //last entry in datafile
|
||||
//free memory
|
||||
csvFile = undefined;
|
||||
times = undefined;
|
||||
|
||||
//steps
|
||||
var csvFile = storage.open(filename, "r");
|
||||
steps = getArrayFromCSV(csvFile, 1);
|
||||
first = first + " " + steps[0] + "/" + setting('stepGoal');
|
||||
last = last + " " + steps[steps.length-1] + "/" + setting('stepGoal');
|
||||
|
||||
//define y-axis grid labels
|
||||
stepsLastEntry = steps[steps.length-1];
|
||||
if (stepsLastEntry < 1000) gridyValue = 100;
|
||||
if (stepsLastEntry >= 1000 && stepsLastEntry < 10000) gridyValue = 1000;
|
||||
if (stepsLastEntry > 10000) gridyValue = 5000;
|
||||
|
||||
//draw
|
||||
drawMenu();
|
||||
g.drawString("First: " + first, 10, 30);
|
||||
g.drawString(" Last: " + last, 10, 40);
|
||||
require("graph").drawLine(g, steps, {
|
||||
//title: "Steps Counted",
|
||||
axes : true,
|
||||
gridy : gridyValue,
|
||||
y : 60, //offset on screen
|
||||
x : 5, //offset on screen
|
||||
});
|
||||
//free memory from big variables
|
||||
allData = undefined;
|
||||
allDataFile = undefined;
|
||||
csvFile = undefined;
|
||||
times = undefined;
|
||||
}
|
||||
|
||||
function drawMenu () {
|
||||
g.clear();
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString("BTN1:Timespan | BTN2:Draw", 20, 10);
|
||||
g.drawString("Timespan: " + history/1000/60/60 + " hours", 20, 20);
|
||||
}
|
||||
|
||||
setWatch(function() { //BTN1
|
||||
switch(history) {
|
||||
case 3600000 : //1h
|
||||
history = 14400000; //4h
|
||||
break;
|
||||
case 86400000 : //24
|
||||
history = 3600000; //1h
|
||||
break;
|
||||
default :
|
||||
history = history + 14400000; //4h
|
||||
break;
|
||||
}
|
||||
drawMenu();
|
||||
}, BTN1, {edge:"rising", debounce:50, repeat:true});
|
||||
|
||||
setWatch(function() { //BTN2
|
||||
g.setFont("6x8", 2);
|
||||
g.drawString ("Drawing...",30,60);
|
||||
drawGraph();
|
||||
}, BTN2, {edge:"rising", debounce:50, repeat:true});
|
||||
|
||||
setWatch(function() { //BTN3
|
||||
}, BTN3, {edge:"rising", debounce:50, repeat:true});
|
||||
|
||||
setWatch(function() { //BTN4
|
||||
}, BTN4, {edge:"rising", debounce:50, repeat:true});
|
||||
|
||||
setWatch(function() { //BTN5
|
||||
}, BTN5, {edge:"rising", debounce:50, repeat:true});
|
||||
|
||||
//load settings
|
||||
let settings;
|
||||
function loadSettings() {
|
||||
settings = storage.readJSON(SETTINGS_FILE, 1) || {};
|
||||
}
|
||||
|
||||
drawMenu();
|
||||
|
||||
})();
|
|
@ -3,13 +3,14 @@
|
|||
var startTimeStep = new Date(); //set start time
|
||||
var stopTimeStep = 0; //Time after one step
|
||||
var timerResetActive = 0; //timer to reset active
|
||||
var timerStoreData = 0; //timer to store data
|
||||
var steps = 0; //steps taken
|
||||
var stepsCounted = 0; //active steps counted
|
||||
var active = 0; //x steps in y seconds achieved
|
||||
var stepGoalPercent = 0; //percentage of step goal
|
||||
var stepGoalBarLength = 0; //length og progress bar
|
||||
var lastUpdate = new Date(); //used to reset counted steps on new day
|
||||
var width = 45; //width of widget
|
||||
var width = 46; //width of widget
|
||||
|
||||
//used for statistics and debugging
|
||||
var stepsTooShort = 0;
|
||||
|
@ -18,13 +19,41 @@
|
|||
|
||||
var distance = 0; //distance travelled
|
||||
|
||||
const s = require('Storage');
|
||||
const SETTINGS_FILE = 'activepedom.settings.json';
|
||||
const PEDOMFILE = "activepedom.steps.json";
|
||||
var dataFile;
|
||||
var storeDataInterval = 5*60*1000; //ms
|
||||
|
||||
let settings;
|
||||
//load settings
|
||||
function loadSettings() {
|
||||
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {};
|
||||
settings = s.readJSON(SETTINGS_FILE, 1) || {};
|
||||
}
|
||||
|
||||
function storeData() {
|
||||
now = new Date();
|
||||
month = now.getMonth() + 1;
|
||||
if (month < 10) month = "0" + month;
|
||||
filename = filename = "activepedom" + now.getFullYear() + month + now.getDate() + ".data";
|
||||
dataFile = s.open(filename,"a");
|
||||
if (dataFile) {
|
||||
if (dataFile.getLength() == 0) {
|
||||
stepsToWrite = 0;
|
||||
}
|
||||
else {
|
||||
stepsToWrite = stepsCounted;
|
||||
}
|
||||
dataFile.write([
|
||||
now.getTime(),
|
||||
stepsToWrite,
|
||||
active,
|
||||
stepsTooShort,
|
||||
stepsTooLong,
|
||||
stepsOutsideTime,
|
||||
].join(",")+"\n");
|
||||
}
|
||||
dataFile = undefined;
|
||||
}
|
||||
|
||||
//return setting
|
||||
|
@ -77,20 +106,20 @@
|
|||
|
||||
//Remove step if time between first and second step is too long
|
||||
if (stepTimeDiff >= setting('cMaxTime')) { //milliseconds
|
||||
stepsTooLong++; //count steps which are note counted, because time too long
|
||||
stepsTooLong++; //count steps which are not counted, because time too long
|
||||
steps--;
|
||||
}
|
||||
|
||||
//Remove step if time between first and second step is too short
|
||||
if (stepTimeDiff <= setting('cMinTime')) { //milliseconds
|
||||
stepsTooShort++; //count steps which are note counted, because time too short
|
||||
stepsTooShort++; //count steps which are not counted, because time too short
|
||||
steps--;
|
||||
}
|
||||
|
||||
//Step threshold reached
|
||||
if (steps >= setting('stepThreshold')) {
|
||||
if (active == 0) {
|
||||
stepsCounted = stepsCounted + (setting('stepThreshold') -1) ; //count steps needed to reach active status, last step is counted anyway, so treshold -1
|
||||
stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reac active status
|
||||
stepsOutsideTime = stepsOutsideTime - 10; //substract steps needed to reach active status
|
||||
}
|
||||
active = 1;
|
||||
clearInterval(timerResetActive); //stop timer which resets active
|
||||
|
@ -109,14 +138,17 @@
|
|||
|
||||
function draw() {
|
||||
var height = 23; //width is deined globally
|
||||
distance = (stepsCounted * setting('stepLength')) / 100 /1000 //distance in km
|
||||
distance = (stepsCounted * setting('stepLength')) / 100 /1000; //distance in km
|
||||
|
||||
//Check if same day
|
||||
let date = new Date();
|
||||
if (lastUpdate.getDate() == date.getDate()){ //if same day
|
||||
}
|
||||
else {
|
||||
stepsCounted = 1; //set stepcount to 1
|
||||
else { //different day, set all steps to 0
|
||||
stepsCounted = 0;
|
||||
stepsTooShort = 0;
|
||||
stepsTooLong = 0;
|
||||
stepsOutsideTime = 0;
|
||||
}
|
||||
lastUpdate = date;
|
||||
|
||||
|
@ -166,7 +198,7 @@
|
|||
stepsTooLong : stepsTooLong,
|
||||
stepsOutsideTime : stepsOutsideTime
|
||||
};
|
||||
require("Storage").write(PEDOMFILE,d); //write array to file
|
||||
s.write(PEDOMFILE,d); //write array to file
|
||||
});
|
||||
|
||||
//When Step is registered by firmware
|
||||
|
@ -182,8 +214,7 @@
|
|||
});
|
||||
|
||||
//Read data from file and set variables
|
||||
let pedomData = require("Storage").readJSON(PEDOMFILE,1);
|
||||
|
||||
let pedomData = s.readJSON(PEDOMFILE,1);
|
||||
if (pedomData) {
|
||||
if (pedomData.lastUpdate) lastUpdate = new Date(pedomData.lastUpdate);
|
||||
stepsCounted = pedomData.stepsToday|0;
|
||||
|
@ -191,12 +222,10 @@
|
|||
stepsTooLong = pedomData.stepsTooLong;
|
||||
stepsOutsideTime = pedomData.stepsOutsideTime;
|
||||
}
|
||||
|
||||
pedomdata = 0; //reset pedomdata to save memory
|
||||
|
||||
setStepSensitivity(setting('stepSensitivity')); //set step sensitivity (80 is standard, 400 is muss less sensitive)
|
||||
|
||||
timerStoreData = setInterval(storeData, storeDataInterval); //store data regularly
|
||||
//Add widget
|
||||
WIDGETS["activepedom"]={area:"tl",width:width,draw:draw};
|
||||
|
||||
})();
|
|
@ -4,3 +4,4 @@
|
|||
0.04: Tweaks for variable size widget system
|
||||
0.05: Add alarm.boot.js and move code from the bootloader
|
||||
0.06: Change 'New Alarm' to 'Save', allow Deletion of Alarms
|
||||
0.07: Don't overwrite existing settings on app update
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: First release
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwMB/4ACx4ED/0DApP8AqAXB84GDg/DAgXj/+DCAUABgIFB4EAv4FCwEAj0PAoJPBgwFEgEfDgMOAoM/AoMegFAAoP8jkA8F/AoM8gP4DgP4nBvD/F4KQfwuAFE+A/CAoPgAofx8A/CKYRwELIIFDLII6BAoZSBLIYeC/0BwAFDgfAGAQFBHgf8g4BBIIUH/wFBSYMPAoXwAog/Bj4FEv4FDDQQCBQoQFCZYYFi/6KE/+P/4A="))
|
|
@ -0,0 +1,314 @@
|
|||
/** Global constants */
|
||||
const DEG_TO_RAD = Math.PI / 180;
|
||||
const EARTH_RADIUS = 6371008.8;
|
||||
|
||||
/** Utilities for handling vectors */
|
||||
class Vector {
|
||||
static magnitude(a) {
|
||||
let sum = 0;
|
||||
for (const key of Object.keys(a)) {
|
||||
sum += a[key] * a[key];
|
||||
}
|
||||
return Math.sqrt(sum);
|
||||
}
|
||||
|
||||
static add(a, b) {
|
||||
const result = {};
|
||||
for (const key of Object.keys(a)) {
|
||||
result[key] = a[key] + b[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static sub(a, b) {
|
||||
const result = {};
|
||||
for (const key of Object.keys(a)) {
|
||||
result[key] = a[key] - b[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static multiplyScalar(a, x) {
|
||||
const result = {};
|
||||
for (const key of Object.keys(a)) {
|
||||
result[key] = a[key] * x;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static divideScalar(a, x) {
|
||||
const result = {};
|
||||
for (const key of Object.keys(a)) {
|
||||
result[key] = a[key] / x;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/** Interquartile range filter, to detect outliers */
|
||||
class IqrFilter {
|
||||
constructor(size, threshold) {
|
||||
const q = Math.floor(size / 4);
|
||||
this._buffer = [];
|
||||
this._size = 4 * q + 2;
|
||||
this._i1 = q;
|
||||
this._i3 = 3 * q + 1;
|
||||
this._threshold = threshold;
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this._buffer.length === this._size;
|
||||
}
|
||||
|
||||
isOutlier(point) {
|
||||
let result = true;
|
||||
if (this._buffer.length === this._size) {
|
||||
result = false;
|
||||
for (const key of Object.keys(point)) {
|
||||
const data = this._buffer.map(item => item[key]);
|
||||
data.sort((a, b) => (a - b) / Math.abs(a - b));
|
||||
const q1 = data[this._i1];
|
||||
const q3 = data[this._i3];
|
||||
const iqr = q3 - q1;
|
||||
const lower = q1 - this._threshold * iqr;
|
||||
const upper = q3 + this._threshold * iqr;
|
||||
if (point[key] < lower || point[key] > upper) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._buffer.push(point);
|
||||
this._buffer = this._buffer.slice(-this._size);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/** Process GPS data */
|
||||
class Gps {
|
||||
constructor() {
|
||||
this._lastCall = Date.now();
|
||||
this._lastValid = 0;
|
||||
this._coords = null;
|
||||
this._filter = new IqrFilter(10, 1.5);
|
||||
this._shift = { x: 0, y: 0, z: 0 };
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this._filter.isReady();
|
||||
}
|
||||
|
||||
getDistance(gps) {
|
||||
const time = Date.now();
|
||||
const interval = (time - this._lastCall) / 1000;
|
||||
this._lastCall = time;
|
||||
|
||||
if (!gps.fix) {
|
||||
return { t: interval, d: 0 };
|
||||
}
|
||||
|
||||
const p = gps.lat * DEG_TO_RAD;
|
||||
const q = gps.lon * DEG_TO_RAD;
|
||||
const coords = {
|
||||
x: EARTH_RADIUS * Math.sin(p) * Math.cos(q),
|
||||
y: EARTH_RADIUS * Math.sin(p) * Math.sin(q),
|
||||
z: EARTH_RADIUS * Math.cos(p),
|
||||
};
|
||||
|
||||
if (!this._coords) {
|
||||
this._coords = coords;
|
||||
this._lastValid = time;
|
||||
return { t: interval, d: 0 };
|
||||
}
|
||||
|
||||
const ds = Vector.sub(coords, this._coords);
|
||||
const dt = (time - this._lastValid) / 1000;
|
||||
const v = Vector.divideScalar(ds, dt);
|
||||
|
||||
if (this._filter.isOutlier(v)) {
|
||||
return { t: interval, d: 0 };
|
||||
}
|
||||
|
||||
this._shift = Vector.add(this._shift, ds);
|
||||
const length = Vector.magnitude(this._shift);
|
||||
const remainder = length % 10;
|
||||
const distance = length - remainder;
|
||||
|
||||
this._coords = coords;
|
||||
this._lastValid = time;
|
||||
if (distance > 0) {
|
||||
this._shift = Vector.multiplyScalar(this._shift, remainder / length);
|
||||
}
|
||||
|
||||
return { t: interval, d: distance };
|
||||
}
|
||||
}
|
||||
|
||||
/** Process step counter data */
|
||||
class Step {
|
||||
constructor(size) {
|
||||
this._buffer = [];
|
||||
this._size = size;
|
||||
}
|
||||
|
||||
getCadence() {
|
||||
this._buffer.push(Date.now() / 1000);
|
||||
this._buffer = this._buffer.slice(-this._size);
|
||||
const interval = this._buffer[this._buffer.length - 1] - this._buffer[0];
|
||||
return interval ? Math.round(60 * (this._buffer.length - 1) / interval) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
const gps = new Gps();
|
||||
const step = new Step(10);
|
||||
|
||||
let totDist = 0;
|
||||
let totTime = 0;
|
||||
let totSteps = 0;
|
||||
|
||||
let speed = 0;
|
||||
let cadence = 0;
|
||||
let heartRate = 0;
|
||||
|
||||
let gpsReady = false;
|
||||
let hrmReady = false;
|
||||
let running = false;
|
||||
|
||||
function formatClock(date) {
|
||||
return ('0' + date.getHours()).substr(-2) + ':' + ('0' + date.getMinutes()).substr(-2);
|
||||
}
|
||||
|
||||
function formatDistance(m) {
|
||||
return ('0' + (m / 1000).toFixed(2) + ' km').substr(-7);
|
||||
}
|
||||
|
||||
function formatTime(s) {
|
||||
const hrs = Math.floor(s / 3600);
|
||||
const min = Math.floor(s / 60);
|
||||
const sec = Math.floor(s % 60);
|
||||
return (hrs ? hrs + ':' : '') + ('0' + min).substr(-2) + `:` + ('0' + sec).substr(-2);
|
||||
}
|
||||
|
||||
function formatSpeed(kmh) {
|
||||
if (kmh <= 0.6) {
|
||||
return `__'__"`;
|
||||
}
|
||||
const skm = 3600 / kmh;
|
||||
const min = Math.floor(skm / 60);
|
||||
const sec = Math.floor(skm % 60);
|
||||
return ('0' + min).substr(-2) + `'` + ('0' + sec).substr(-2) + `"`;
|
||||
}
|
||||
|
||||
function drawBackground() {
|
||||
g.setColor(running ? 0x00E0 : 0x0000);
|
||||
g.fillRect(0, 30, 240, 240);
|
||||
|
||||
g.setColor(0xFFFF);
|
||||
g.setFontAlign(0, -1, 0);
|
||||
g.setFont('6x8', 2);
|
||||
|
||||
g.drawString('DISTANCE', 120, 50);
|
||||
g.drawString('TIME', 60, 100);
|
||||
g.drawString('PACE', 180, 100);
|
||||
g.drawString('STEPS', 60, 150);
|
||||
g.drawString('STP/m', 180, 150);
|
||||
g.drawString('SPEED', 40, 200);
|
||||
g.drawString('HEART', 120, 200);
|
||||
g.drawString('CADENCE', 200, 200);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const totSpeed = totTime ? 3.6 * totDist / totTime : 0;
|
||||
const totCadence = totTime ? Math.round(60 * totSteps / totTime) : 0;
|
||||
|
||||
g.setColor(running ? 0x00E0 : 0x0000);
|
||||
g.fillRect(0, 30, 240, 50);
|
||||
g.fillRect(0, 70, 240, 100);
|
||||
g.fillRect(0, 120, 240, 150);
|
||||
g.fillRect(0, 170, 240, 200);
|
||||
g.fillRect(0, 220, 240, 240);
|
||||
|
||||
g.setFont('6x8', 2);
|
||||
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.setColor(gpsReady ? 0x07E0 : 0xF800);
|
||||
g.drawString(' GPS', 6, 30);
|
||||
|
||||
g.setFontAlign(1, -1, 0);
|
||||
g.setColor(0xFFFF);
|
||||
g.drawString(formatClock(new Date()), 234, 30);
|
||||
|
||||
g.setFontAlign(0, -1, 0);
|
||||
g.setFontVector(20);
|
||||
g.drawString(formatDistance(totDist), 120, 70);
|
||||
g.drawString(formatTime(totTime), 60, 120);
|
||||
g.drawString(formatSpeed(totSpeed), 180, 120);
|
||||
g.drawString(totSteps, 60, 170);
|
||||
g.drawString(totCadence, 180, 170);
|
||||
|
||||
g.setFont('6x8', 2);
|
||||
g.drawString(formatSpeed(speed), 40, 220);
|
||||
|
||||
g.setColor(hrmReady ? 0x07E0 : 0xF800);
|
||||
g.drawString(heartRate, 120, 220);
|
||||
|
||||
g.setColor(0xFFFF);
|
||||
g.drawString(cadence, 200, 220);
|
||||
}
|
||||
|
||||
function handleGps(coords) {
|
||||
const step = gps.getDistance(coords);
|
||||
gpsReady = coords.fix > 0 && gps.isReady();
|
||||
speed = isFinite(gps.speed) ? gps.speed : 0;
|
||||
if (running) {
|
||||
totDist += step.d;
|
||||
totTime += step.t;
|
||||
}
|
||||
}
|
||||
|
||||
function handleHrm(hrm) {
|
||||
hrmReady = hrm.confidence > 50;
|
||||
heartRate = hrm.bpm;
|
||||
}
|
||||
|
||||
function handleStep() {
|
||||
cadence = step.getCadence();
|
||||
if (running) {
|
||||
totSteps += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function start() {
|
||||
running = true;
|
||||
drawBackground();
|
||||
draw();
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!running) {
|
||||
totDist = 0;
|
||||
totTime = 0;
|
||||
totSteps = 0;
|
||||
}
|
||||
running = false;
|
||||
drawBackground();
|
||||
draw();
|
||||
}
|
||||
|
||||
Bangle.on('GPS', handleGps);
|
||||
Bangle.on('HRM', handleHrm);
|
||||
Bangle.on('step', handleStep);
|
||||
|
||||
Bangle.setGPSPower(1);
|
||||
Bangle.setHRMPower(1);
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
drawBackground();
|
||||
draw();
|
||||
|
||||
setInterval(draw, 500);
|
||||
|
||||
setWatch(start, BTN1, { repeat: true });
|
||||
setWatch(stop, BTN3, { repeat: true });
|
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"windows"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
# BuffGym
|
||||
|
||||
This gym training assistant trains you on the famous [Stronglifts 5x5 workout](https://stronglifts.com/5x5) program.
|
||||
|
||||
## Usage
|
||||
|
||||
When you start the app it will wait on a splash screen until you are ready to start the work out. Press any of the buttons to start
|
||||
|
||||

|
||||
|
||||
You are then presented with the programs menu, use BTN1 to move up the list, and BTN3 to move down the list. Once you have made your selection, press BTN2 to select the program.
|
||||
|
||||

|
||||
|
||||
You will now begin moving through the exercises in the program. You will see the exercise information on the display.
|
||||
1. At the top is the exercise name, e.g 'Squats'
|
||||
2. Next is the weight you must train
|
||||
3. In the center is where you record the number of *reps* you completed (more on that shortly)
|
||||
4. Below the *reps* value, is the target reps you must try to reach.
|
||||
5. Below the target reps is the current set you are training, out of the total sets for the exercise.
|
||||
6. The *reps* value is used to store what you achieved for the current set, you enter this after you have trained on your current set. To alter this value, use BTN1 to increase the value (it will stop at the maximum required reps) and BTN3 to decreas the value to a minimum of 0 (this is the default value). Pressing BTN2 will confirm your reps
|
||||
|
||||

|
||||
|
||||
You will then be presented with a rest timer screen, it counts down and automatically moves to the next exercise when it reaches 0. You can cancel the timer early if you wish by pressing BTN2. If it is the last set of an exercise, you don't need to rest, so it lets you know you have completed all the sets in the exercise and can start the next exercise.
|
||||
|
||||

|
||||

|
||||
|
||||
Once all exercises are done, you are presented with a pat-on-the-back screen to tell you how awesome you are.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
* If you successfully complete all reps and sets for an exercise, it will automatically update your weights for next time
|
||||
* Has a neat rest timer to make sure you are training optimally
|
||||
* Doesn't require a mobile phone, most 'smart watches' are just a visual presentation of the mobile phone app, this runs purley on the watch. So why not leave your phone and its distractions out of the gym!
|
||||
* Clear and simple user interface
|
||||
|
||||
## Created by
|
||||
|
||||
[Paul Cockrell](https://github.com/paulcockrell) April 2020.
|
|
@ -0,0 +1,144 @@
|
|||
exports = class Exercise {
|
||||
constructor(params) {
|
||||
this.title = params.title;
|
||||
this.weight = params.weight;
|
||||
this.unit = params.unit;
|
||||
this.restPeriod = params.restPeriod;
|
||||
this.completed = false;
|
||||
this.sets = [];
|
||||
this._restTimeout = null;
|
||||
this._restInterval = null;
|
||||
this._state = null;
|
||||
this._originalRestPeriod = params.restPeriod;
|
||||
this._weightIncrement = params.weightIncrement || 2.5;
|
||||
}
|
||||
|
||||
get humanTitle() {
|
||||
return `${this.title} ${this.weight}${this.unit}`;
|
||||
}
|
||||
|
||||
get subTitle() {
|
||||
const totalSets = this.sets.length;
|
||||
const uncompletedSets = this.sets.filter((set) => !set.isCompleted()).length;
|
||||
const currentSet = (totalSets - uncompletedSets) + 1;
|
||||
return `Set ${currentSet} of ${totalSets}`;
|
||||
}
|
||||
|
||||
decRestPeriod() {
|
||||
this.restPeriod--;
|
||||
}
|
||||
|
||||
addSet(set) {
|
||||
this.sets.push(set);
|
||||
}
|
||||
|
||||
currentSet() {
|
||||
return this.sets.filter(set => !set.isCompleted())[0];
|
||||
}
|
||||
|
||||
isLastSet() {
|
||||
return this.sets.filter(set => !set.isCompleted()).length === 1;
|
||||
}
|
||||
|
||||
isCompleted() {
|
||||
return !!this.completed;
|
||||
}
|
||||
|
||||
canSetCompleted() {
|
||||
return this.sets.filter(set => set.isCompleted()).length === this.sets.length;
|
||||
}
|
||||
|
||||
setCompleted() {
|
||||
if (!this.canSetCompleted()) throw "All sets must be completed";
|
||||
if (this.canProgress()) this.weight += this._weightIncrement;
|
||||
this.completed = true;
|
||||
}
|
||||
|
||||
canProgress() {
|
||||
let completedRepsTotalSum = 0;
|
||||
let targetRepsTotalSum = 0;
|
||||
this.sets.forEach(set => completedRepsTotalSum += set.reps);
|
||||
this.sets.forEach(set => targetRepsTotalSum += set.maxReps);
|
||||
|
||||
return (targetRepsTotalSum - completedRepsTotalSum) === 0;
|
||||
}
|
||||
|
||||
startRestTimer(program) {
|
||||
this._restTimeout = setTimeout(() => {
|
||||
this.next(program);
|
||||
}, 1000 * this.restPeriod);
|
||||
|
||||
this._restInterval = setInterval(() => {
|
||||
program.emit("redraw");
|
||||
}, 1000 );
|
||||
}
|
||||
|
||||
resetRestTimer() {
|
||||
clearTimeout(this._restTimeout);
|
||||
clearInterval(this._restInterval);
|
||||
this._restTimeout = null;
|
||||
this._restInterval = null;
|
||||
this.restPeriod = this._originalRestPeriod;
|
||||
}
|
||||
|
||||
isRestTimerRunning() {
|
||||
return this._restTimeout != null;
|
||||
}
|
||||
|
||||
setupStartedButtons(program) {
|
||||
clearWatch();
|
||||
|
||||
setWatch(() => {
|
||||
this.currentSet().incReps();
|
||||
program.emit("redraw");
|
||||
}, BTN1, {repeat: true});
|
||||
|
||||
setWatch(program.next.bind(program), BTN2, {repeat: false});
|
||||
|
||||
setWatch(() => {
|
||||
this.currentSet().decReps();
|
||||
program.emit("redraw");
|
||||
}, BTN3, {repeat: true});
|
||||
}
|
||||
|
||||
setupRestingButtons(program) {
|
||||
clearWatch();
|
||||
setWatch(program.next.bind(program), BTN2, {repeat: false});
|
||||
}
|
||||
|
||||
next(program) {
|
||||
const STARTED = 1;
|
||||
const RESTING = 2;
|
||||
const COMPLETED = 3;
|
||||
|
||||
switch(this._state) {
|
||||
case null:
|
||||
this._state = STARTED;
|
||||
this.setupStartedButtons(program);
|
||||
break;
|
||||
case STARTED:
|
||||
this._state = RESTING;
|
||||
this.startRestTimer(program);
|
||||
this.setupRestingButtons(program);
|
||||
break;
|
||||
case RESTING:
|
||||
this.resetRestTimer();
|
||||
this.currentSet().setCompleted();
|
||||
|
||||
if (this.canSetCompleted()) {
|
||||
this._state = COMPLETED;
|
||||
this.setCompleted();
|
||||
} else {
|
||||
this._state = null;
|
||||
}
|
||||
// As we are changing state and require it to be reprocessed
|
||||
// invoke the next step of program
|
||||
program.next();
|
||||
break;
|
||||
default:
|
||||
throw "Exercise: Attempting to move to an unknown state";
|
||||
}
|
||||
|
||||
program.emit("redraw");
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+ACPI5AUSADAtB5vNGFQtBAIfNF95hoF4wwoF5AwmF5BhmXYbAEF/6QbF1QwIF04qB54ADAwIwoF4oRKBoIvsB4gvZ58kkgCDFxoxaF5wuHGDQcMF5IwXDZwLDGDmlDIWlkgJDSwIABCRAwPDQohCFgIABDQIOCFwYABr4RCCQIvQDYguEAAwtFF5owJDZAvHFw4vFOYQvKFAowMBxIvFMQwvPAB4wFUQ4vJGDYvUGC4vNdgyuEGDIsNFwYwGNAgAPExAvMGIdfTIovfTpYvrfRCOkZ44ugF44NGF05gUFyQvKGIoueGKIufGJ4uhG5oupGItfr4vvAAgvlGAQvt/wrEF9oEGF841IF9QGHX0oGIAD8kAAYJOFzwEBBQoMFACA="))
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"title": "Program A",
|
||||
"exercises": [
|
||||
{
|
||||
"title": "Squats",
|
||||
"weight": 40,
|
||||
"unit": "Kg",
|
||||
"sets": [5, 5, 5, 5, 5],
|
||||
"restPeriod": 90
|
||||
},
|
||||
{
|
||||
"title": "Overhead press",
|
||||
"weight": 20,
|
||||
"unit": "Kg",
|
||||
"sets": [5, 5, 5, 5, 5],
|
||||
"restPeriod": 90
|
||||
},
|
||||
{
|
||||
"title": "Deadlift",
|
||||
"weight": 20,
|
||||
"unit": "Kg",
|
||||
"sets": [5],
|
||||
"restPeriod": 90
|
||||
},
|
||||
{
|
||||
"title": "Pullups",
|
||||
"weight": 0,
|
||||
"unit": "Kg",
|
||||
"sets": [10, 10, 10],
|
||||
"restPeriod": 90
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"title": "Program B",
|
||||
"exercises": [
|
||||
{
|
||||
"title": "Squats",
|
||||
"weight": 40,
|
||||
"unit": "Kg",
|
||||
"sets": [5, 5, 5, 5, 5],
|
||||
"restPeriod": 90
|
||||
},
|
||||
{
|
||||
"title": "Bench press",
|
||||
"weight": 20,
|
||||
"unit": "Kg",
|
||||
"sets": [5, 5, 5, 5, 5],
|
||||
"restPeriod": 90
|
||||
},
|
||||
{
|
||||
"title": "Row",
|
||||
"weight": 20,
|
||||
"unit":"Kg",
|
||||
"sets": [5, 5, 5, 5, 5],
|
||||
"restPeriod": 90
|
||||
},
|
||||
{
|
||||
"title": "Tricep extension",
|
||||
"weight": 20,
|
||||
"unit": "Kg",
|
||||
"sets": [10, 10, 10],
|
||||
"restPeriod": 90
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"title": "Program A",
|
||||
"file": "buffgym-program-a.json"
|
||||
},
|
||||
{
|
||||
"title": "Program B",
|
||||
"file": "buffgym-program-b.json"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,56 @@
|
|||
exports = class Program {
|
||||
constructor(params) {
|
||||
this.title = params.title;
|
||||
this.exercises = [];
|
||||
this.completed = false;
|
||||
this.on("redraw", redraw.bind(null, this));
|
||||
}
|
||||
|
||||
addExercises(exercises) {
|
||||
exercises.forEach(exercise => this.exercises.push(exercise));
|
||||
}
|
||||
|
||||
currentExercise() {
|
||||
return this.exercises.filter(exercise => !exercise.isCompleted())[0];
|
||||
}
|
||||
|
||||
canComplete() {
|
||||
return this.exercises.filter(exercise => exercise.isCompleted()).length === this.exercises.length;
|
||||
}
|
||||
|
||||
setCompleted() {
|
||||
if (!this.canComplete()) throw "All exercises must be completed";
|
||||
this.completed = true;
|
||||
}
|
||||
|
||||
isCompleted() {
|
||||
return !!this.completed;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
title: this.title,
|
||||
exercises: this.exercises.map(exercise => {
|
||||
return {
|
||||
title: exercise.title,
|
||||
weight: exercise.weight,
|
||||
unit: exercise.unit,
|
||||
sets: exercise.sets.map(set => set.maxReps),
|
||||
restPeriod: exercise.restPeriod,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// State machine
|
||||
next() {
|
||||
if (this.canComplete()) {
|
||||
this.setCompleted();
|
||||
this.emit("redraw");
|
||||
return;
|
||||
}
|
||||
|
||||
// Call current exercise state machine
|
||||
this.currentExercise().next(this);
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.8 KiB |
|
@ -0,0 +1,28 @@
|
|||
exports = class Set {
|
||||
constructor(maxReps) {
|
||||
this.minReps = 0;
|
||||
this.maxReps = maxReps;
|
||||
this.reps = 0;
|
||||
this.completed = false;
|
||||
}
|
||||
|
||||
isCompleted() {
|
||||
return !!this.completed;
|
||||
}
|
||||
|
||||
setCompleted() {
|
||||
this.completed = true;
|
||||
}
|
||||
|
||||
incReps() {
|
||||
if (this.completed) return;
|
||||
if (this.reps >= this.maxReps) return;
|
||||
this.reps++;
|
||||
}
|
||||
|
||||
decReps() {
|
||||
if (this.completed) return;
|
||||
if (this.reps <= this.minReps) return;
|
||||
this.reps--;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,288 @@
|
|||
/**
|
||||
* BangleJS Stronglifts 5x5 training aid
|
||||
*
|
||||
* Original Author: Paul Cockrell https://github.com/paulcockrell
|
||||
* Created: April 2020
|
||||
*
|
||||
* Inspired by:
|
||||
* - Stronglifts 5x5 training program https://stronglifts.com/5x5/
|
||||
* - Stronglifts smart watch app
|
||||
*/
|
||||
|
||||
Bangle.setLCDMode("120x120");
|
||||
|
||||
const W = g.getWidth();
|
||||
const H = g.getHeight();
|
||||
const RED = "#d32e29";
|
||||
const PINK = "#f05a56";
|
||||
const WHITE = "#ffffff";
|
||||
|
||||
function drawMenu(params) {
|
||||
const hs = require("heatshrink");
|
||||
const incImg = hs.decompress(atob("gsFwMAkM+oUA"));
|
||||
const decImg = hs.decompress(atob("gsFwIEBnwCBA"));
|
||||
const okImg = hs.decompress(atob("gsFwMAhGFo0A"));
|
||||
const DEFAULT_PARAMS = {
|
||||
showBTN1: false,
|
||||
showBTN2: false,
|
||||
showBTN3: false,
|
||||
};
|
||||
const p = Object.assign({}, DEFAULT_PARAMS, params);
|
||||
if (p.showBTN1) g.drawImage(incImg, W - 10, 10);
|
||||
if (p.showBTN2) g.drawImage(okImg, W - 10, 60);
|
||||
if (p.showBTN3) g.drawImage(decImg, W - 10, 110);
|
||||
}
|
||||
|
||||
function drawSet(exercise) {
|
||||
const set = exercise.currentSet();
|
||||
if (set.isCompleted()) return;
|
||||
|
||||
g.clear();
|
||||
|
||||
// Draw exercise title
|
||||
g.setColor(PINK);
|
||||
g.fillRect(15, 0, W - 15, 18);
|
||||
g.setFontAlign(0, -1);
|
||||
g.setFont("6x8", 1);
|
||||
g.setColor(WHITE);
|
||||
g.drawString(exercise.title, W / 2, 5);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString(exercise.weight + " " + exercise.unit, W / 2, 27);
|
||||
// Draw completed reps counter
|
||||
g.setFontAlign(0, 0);
|
||||
g.setColor(PINK);
|
||||
g.fillRect(15, 42, W - 15, 80);
|
||||
g.setColor(WHITE);
|
||||
g.setFont("6x8", 5);
|
||||
g.drawString(set.reps, (W / 2) + 2, (H / 2) + 1);
|
||||
g.setFont("6x8", 1);
|
||||
const note = `Target reps: ${set.maxReps}`;
|
||||
g.drawString(note, W / 2, H - 24);
|
||||
// Draw sets monitor
|
||||
g.drawString(exercise.subTitle, W / 2, H - 12);
|
||||
|
||||
drawMenu({showBTN1: true, showBTN2: true, showBTN3: true});
|
||||
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function drawProgDone() {
|
||||
const title1 = "You did";
|
||||
const title2 = "GREAT!";
|
||||
const msg = "That's the program\ncompleted. Now eat\nsome food and\nget plenty of rest.";
|
||||
|
||||
clearWatch();
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat: false});
|
||||
drawMenu({showBTN2: true});
|
||||
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(WHITE);
|
||||
g.setFont("6x8", 2);
|
||||
g.drawString(title1, W / 2, 10);
|
||||
g.drawString(title2, W / 2, 30);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString(msg, (W / 2) + 3, 70);
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function drawSetComp() {
|
||||
const title = "Good work";
|
||||
const msg = "No need to rest\nmove straight on\nto the next\nexercise.Your\nweight has been\nincreased for\nnext time!";
|
||||
|
||||
g.clear();
|
||||
drawMenu({showBTN2: true});
|
||||
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(WHITE);
|
||||
g.setFont("6x8", 2);
|
||||
g.drawString(title, W / 2, 10);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString(msg, (W / 2) - 2, 45);
|
||||
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function drawRestTimer(program) {
|
||||
const exercise = program.currentExercise();
|
||||
const motivation = "Take a breather..";
|
||||
|
||||
if (exercise.restPeriod <= 0) {
|
||||
exercise.resetRestTimer();
|
||||
program.next();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
g.clear();
|
||||
drawMenu({showBTN2: true});
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(PINK);
|
||||
g.fillRect(15, 42, W - 15, 80);
|
||||
g.setColor(WHITE);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString("Have a short\nrest period.", W / 2, 10);
|
||||
g.setFont("6x8", 5);
|
||||
g.drawString(exercise.restPeriod, (W / 2) + 2, (H / 2) - 19);
|
||||
g.flip();
|
||||
|
||||
exercise.decRestPeriod();
|
||||
}
|
||||
|
||||
function redraw(program) {
|
||||
const exercise = program.currentExercise();
|
||||
g.clear();
|
||||
|
||||
if (program.isCompleted()) {
|
||||
saveProg(program);
|
||||
drawProgDone(program);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exercise.isRestTimerRunning()) {
|
||||
if (exercise.isLastSet()) {
|
||||
drawSetComp(program);
|
||||
} else {
|
||||
drawRestTimer(program);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
drawSet(exercise);
|
||||
}
|
||||
|
||||
function drawProgMenu(programs, selProgIdx) {
|
||||
g.clear();
|
||||
g.setFontAlign(0, -1);
|
||||
g.setColor(WHITE);
|
||||
g.setFont("6x8", 2);
|
||||
g.drawString("BuffGym", W / 2, 10);
|
||||
|
||||
g.setFont("6x8", 1);
|
||||
g.setFontAlign(-1, -1);
|
||||
let selectedProgram = programs[selProgIdx].title;
|
||||
let yPos = 50;
|
||||
programs.forEach(program => {
|
||||
g.setColor("#f05a56");
|
||||
g.fillRect(0, yPos, W, yPos + 11);
|
||||
g.setColor("#ffffff");
|
||||
if (selectedProgram === program.title) {
|
||||
g.drawRect(0, yPos, W - 1, yPos + 11);
|
||||
}
|
||||
g.drawString(program.title, 10, yPos + 2);
|
||||
yPos += 15;
|
||||
});
|
||||
g.flip();
|
||||
}
|
||||
|
||||
function setupMenu() {
|
||||
clearWatch();
|
||||
const progs = getProgIndex();
|
||||
let selProgIdx = 0;
|
||||
drawProgMenu(progs, selProgIdx);
|
||||
|
||||
setWatch(()=>{
|
||||
selProgIdx--;
|
||||
if (selProgIdx< 0) selProgIdx = 0;
|
||||
drawProgMenu(progs, selProgIdx);
|
||||
}, BTN1, {repeat: true});
|
||||
|
||||
setWatch(()=>{
|
||||
const prog = buildProg(progs[selProgIdx].file);
|
||||
prog.next();
|
||||
}, BTN2, {repeat: false});
|
||||
|
||||
setWatch(()=>{
|
||||
selProgIdx++;
|
||||
if (selProgIdx > progs.length - 1) selProgIdx = progs.length - 1;
|
||||
drawProgMenu(progs, selProgIdx);
|
||||
}, BTN3, {repeat: true});
|
||||
}
|
||||
|
||||
function drawSplash() {
|
||||
g.reset();
|
||||
g.setBgColor(RED);
|
||||
g.clear();
|
||||
g.setColor(WHITE);
|
||||
g.setFontAlign(0,-1);
|
||||
g.setFont("6x8", 2);
|
||||
g.drawString("BuffGym", W / 2, 10);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString("5x5", W / 2, 42);
|
||||
g.drawString("training app", W / 2, 55);
|
||||
g.drawRect(19, 38, 100, 99);
|
||||
const img = require("heatshrink").decompress(atob("lkdxH+AB/I5ASQACwpB5vNFkwpBAIfNFdZZkFYwskFZAsiFZBZiVYawEFf6ETFUwsIFUYmB54ADAwIskFYoRKBoIroB4grV58kkgCDFRotWFZwqHFiwYMFZIsTC5wLDFjGlCoWlkgJDRQIABCRAsLCwodCFAIABCwIOCFQYABr4RCCQIrMC4gqEAAwpFFZosFC5ArHFQ4rFNYQrGEgosMBxIrFLQwrLAB4sFSw4rFFjYrQFi4rNbASeEFjIoJFQYsGMAgAPEQgAIGwosCRoorbA="));
|
||||
g.drawImage(img, 40, 70);
|
||||
g.flip();
|
||||
|
||||
let flasher = false;
|
||||
let bgCol, txtCol;
|
||||
const i = setInterval(() => {
|
||||
if (flasher) {
|
||||
bgCol = WHITE;
|
||||
txtCol = RED;
|
||||
} else {
|
||||
bgCol = RED;
|
||||
txtCol = WHITE;
|
||||
}
|
||||
flasher = !flasher;
|
||||
g.setColor(bgCol);
|
||||
g.fillRect(0, 108, W, 120);
|
||||
g.setColor(txtCol);
|
||||
g.drawString("Press btn to begin", W / 2, 110);
|
||||
g.flip();
|
||||
}, 250);
|
||||
|
||||
setWatch(()=>{
|
||||
clearInterval(i);
|
||||
setupMenu();
|
||||
}, BTN1, {repeat: false});
|
||||
|
||||
setWatch(()=>{
|
||||
clearInterval(i);
|
||||
setupMenu();
|
||||
}, BTN2, {repeat: false});
|
||||
|
||||
setWatch(()=>{
|
||||
clearInterval(i);
|
||||
setupMenu();
|
||||
}, BTN3, {repeat: false});
|
||||
}
|
||||
|
||||
function getProgIndex() {
|
||||
const progIdx = require("Storage").readJSON("buffgym-program-index.json");
|
||||
return progIdx;
|
||||
}
|
||||
|
||||
function buildProg(fName) {
|
||||
const Set = require("buffgym-set.js");
|
||||
const Exercise = require("buffgym-exercise.js");
|
||||
const Program = require("buffgym-program.js");
|
||||
const progJSON = require("Storage").readJSON(fName);
|
||||
const prog = new Program({
|
||||
title: progJSON.title,
|
||||
});
|
||||
const exercises = progJSON.exercises.map(exerciseJSON => {
|
||||
const exercise = new Exercise({
|
||||
title: exerciseJSON.title,
|
||||
weight: exerciseJSON.weight,
|
||||
unit: exerciseJSON.unit,
|
||||
restPeriod: exerciseJSON.restPeriod,
|
||||
});
|
||||
exerciseJSON.sets.forEach(setJSON => {
|
||||
exercise.addSet(new Set(setJSON));
|
||||
});
|
||||
|
||||
return exercise;
|
||||
});
|
||||
prog.addExercises(exercises);
|
||||
|
||||
return prog;
|
||||
}
|
||||
|
||||
function saveProg(program) {
|
||||
const fName = getProgIndex().find(prog => prog.title === program.title).file;
|
||||
require("Storage").writeJSON(fName, program.toJSON());
|
||||
}
|
||||
|
||||
drawSplash();
|
After Width: | Height: | Size: 7.4 KiB |
|
@ -1,2 +1,3 @@
|
|||
0.01: New widget and app!
|
||||
0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme)
|
||||
0.01: New widget and app!
|
||||
0.02: Setting to reset values, timer buzzes at 00:00 and not later (see readme)
|
||||
0.03: Display only minutes:seconds when less than 1 hour left
|
|
@ -1,93 +1,93 @@
|
|||
(() => {
|
||||
const storage = require('Storage');
|
||||
settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file
|
||||
var height = 23;
|
||||
var width = 58;
|
||||
var interval = 0; //used for the 1 second interval timer
|
||||
var now = new Date();
|
||||
|
||||
var time = 0;
|
||||
var diff = settingsChronowid.goal - now;
|
||||
|
||||
//Convert ms to time
|
||||
function getTime(t) {
|
||||
var milliseconds = parseInt((t % 1000) / 100),
|
||||
seconds = Math.floor((t / 1000) % 60),
|
||||
minutes = Math.floor((t / (1000 * 60)) % 60),
|
||||
hours = Math.floor((t / (1000 * 60 * 60)) % 24);
|
||||
|
||||
hours = (hours < 10) ? "0" + hours : hours;
|
||||
minutes = (minutes < 10) ? "0" + minutes : minutes;
|
||||
seconds = (seconds < 10) ? "0" + seconds : seconds;
|
||||
|
||||
return hours + ":" + minutes + ":" + seconds;
|
||||
}
|
||||
|
||||
function printDebug() {
|
||||
print ("Nowtime: " + getTime(now));
|
||||
print ("Now: " + now);
|
||||
print ("Goaltime: " + getTime(settingsChronowid.goal));
|
||||
print ("Goal: " + settingsChronowid.goal);
|
||||
print("Difftime: " + getTime(diff));
|
||||
print("Diff: " + diff);
|
||||
print ("Started: " + settingsChronowid.started);
|
||||
print ("----");
|
||||
}
|
||||
|
||||
//counts down, calculates and displays
|
||||
function countDown() {
|
||||
now = new Date();
|
||||
diff = settingsChronowid.goal - now; //calculate difference
|
||||
WIDGETS["chronowid"].draw();
|
||||
//time is up
|
||||
if (settingsChronowid.started && diff < 1000) {
|
||||
Bangle.buzz(1500);
|
||||
//write timer off to file
|
||||
settingsChronowid.started = false;
|
||||
storage.writeJSON('chronowid.json', settingsChronowid);
|
||||
clearInterval(interval); //stop interval
|
||||
}
|
||||
//printDebug();
|
||||
}
|
||||
|
||||
// draw your widget
|
||||
function draw() {
|
||||
if (!settingsChronowid.started) {
|
||||
width = 0;
|
||||
return; //do not draw anything if timer is not started
|
||||
}
|
||||
g.reset();
|
||||
if (diff >= 0) {
|
||||
if (diff < 600000) { //less than 1 hour left
|
||||
width = 58;
|
||||
g.clearRect(this.x,this.y,this.x+width,this.y+height);
|
||||
g.setFont("6x8", 2);
|
||||
g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00
|
||||
}
|
||||
if (diff >= 600000) { //one hour or more left
|
||||
width = 48;
|
||||
g.clearRect(this.x,this.y,this.x+width,this.y+height);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00
|
||||
}
|
||||
}
|
||||
// not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed.
|
||||
// else {
|
||||
// width = 58;
|
||||
// g.clearRect(this.x,this.y,this.x+width,this.y+height);
|
||||
// g.setFont("6x8", 2);
|
||||
// g.drawString("END", this.x+15, this.y+5);
|
||||
// }
|
||||
}
|
||||
|
||||
if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second
|
||||
|
||||
// add the widget
|
||||
WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() {
|
||||
reload();
|
||||
Bangle.drawWidgets(); // relayout all widgets
|
||||
}};
|
||||
|
||||
//printDebug();
|
||||
countDown();
|
||||
(() => {
|
||||
const storage = require('Storage');
|
||||
settingsChronowid = storage.readJSON("chronowid.json",1)||{}; //read settingsChronowid from file
|
||||
var height = 23;
|
||||
var width = 58;
|
||||
var interval = 0; //used for the 1 second interval timer
|
||||
var now = new Date();
|
||||
|
||||
var time = 0;
|
||||
var diff = settingsChronowid.goal - now;
|
||||
|
||||
//Convert ms to time
|
||||
function getTime(t) {
|
||||
var milliseconds = parseInt((t % 1000) / 100),
|
||||
seconds = Math.floor((t / 1000) % 60),
|
||||
minutes = Math.floor((t / (1000 * 60)) % 60),
|
||||
hours = Math.floor((t / (1000 * 60 * 60)) % 24);
|
||||
|
||||
hours = (hours < 10) ? "0" + hours : hours;
|
||||
minutes = (minutes < 10) ? "0" + minutes : minutes;
|
||||
seconds = (seconds < 10) ? "0" + seconds : seconds;
|
||||
|
||||
return hours + ":" + minutes + ":" + seconds;
|
||||
}
|
||||
|
||||
function printDebug() {
|
||||
print ("Nowtime: " + getTime(now));
|
||||
print ("Now: " + now);
|
||||
print ("Goaltime: " + getTime(settingsChronowid.goal));
|
||||
print ("Goal: " + settingsChronowid.goal);
|
||||
print("Difftime: " + getTime(diff));
|
||||
print("Diff: " + diff);
|
||||
print ("Started: " + settingsChronowid.started);
|
||||
print ("----");
|
||||
}
|
||||
|
||||
//counts down, calculates and displays
|
||||
function countDown() {
|
||||
now = new Date();
|
||||
diff = settingsChronowid.goal - now; //calculate difference
|
||||
WIDGETS["chronowid"].draw();
|
||||
//time is up
|
||||
if (settingsChronowid.started && diff < 1000) {
|
||||
Bangle.buzz(1500);
|
||||
//write timer off to file
|
||||
settingsChronowid.started = false;
|
||||
storage.writeJSON('chronowid.json', settingsChronowid);
|
||||
clearInterval(interval); //stop interval
|
||||
}
|
||||
//printDebug();
|
||||
}
|
||||
|
||||
// draw your widget
|
||||
function draw() {
|
||||
if (!settingsChronowid.started) {
|
||||
width = 0;
|
||||
return; //do not draw anything if timer is not started
|
||||
}
|
||||
g.reset();
|
||||
if (diff >= 0) {
|
||||
if (diff < 3600000) { //less than 1 hour left
|
||||
width = 58;
|
||||
g.clearRect(this.x,this.y,this.x+width,this.y+height);
|
||||
g.setFont("6x8", 2);
|
||||
g.drawString(getTime(diff).substring(3), this.x+1, this.y+5); //remove hour part 00:00:00 -> 00:00
|
||||
}
|
||||
if (diff >= 3600000) { //one hour or more left
|
||||
width = 48;
|
||||
g.clearRect(this.x,this.y,this.x+width,this.y+height);
|
||||
g.setFont("6x8", 1);
|
||||
g.drawString(getTime(diff), this.x+1, this.y+((height/2)-4)); //display hour 00:00:00
|
||||
}
|
||||
}
|
||||
// not needed anymoe, because we check if diff < 1000 now, so 00:00 is displayed.
|
||||
// else {
|
||||
// width = 58;
|
||||
// g.clearRect(this.x,this.y,this.x+width,this.y+height);
|
||||
// g.setFont("6x8", 2);
|
||||
// g.drawString("END", this.x+15, this.y+5);
|
||||
// }
|
||||
}
|
||||
|
||||
if (settingsChronowid.started) interval = setInterval(countDown, 1000); //start countdown each second
|
||||
|
||||
// add the widget
|
||||
WIDGETS["chronowid"]={area:"bl",width:width,draw:draw,reload:function() {
|
||||
reload();
|
||||
Bangle.drawWidgets(); // relayout all widgets
|
||||
}};
|
||||
|
||||
//printDebug();
|
||||
countDown();
|
||||
})();
|
|
@ -5,3 +5,5 @@
|
|||
0.05: Tweaks for variable size widget system
|
||||
0.06: Ensure widget update itself (fix #118) and change to using icons
|
||||
0.07: Added @jeffmer's awesome track viewer
|
||||
0.08: Don't overwrite existing settings on app update
|
||||
Clean up recorded tracks on app removal
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,14 @@
|
|||
# QTH Locator
|
||||
|
||||
Convert your current GPS location to the [Maidenhead](https://en.wikipedia.org/wiki/Maidenhead_Locator_System) locator system used by HAM amateur radio operators.
|
||||
|
||||
## Description
|
||||
|
||||
A Maidenhead locator compresses latitude and longitude into a short string of characters, which is similar in concept to the World Geographic Reference System or GEOREF. This position information is presented in a limited level of precision to limit the number of characters needed for its transmission using voice, Morse code, or any other operating mode.
|
||||
|
||||
The chosen coding uses alternating pairs of letters and digits, like so:
|
||||
|
||||
* BL11bh
|
||||
##
|
||||
* support Paul Brewer KI6CQ HamGridSquare.js
|
||||
* support Chris Veness 2002-2012 LatLon library
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4ACwVeAAM6nQECJsYqDFYItCAgQ0DFrxWE2ZhBxOJA4ICBGwhbbxGtAYOz2etnWt2YwBnQCBGgWtxBjXJQQpBLgQABEQIDBMAgwE2YYBwRcUxOtDwgABGgYvGTQQLCMSRNGEQaODF4SQDHgYwUFIlfAgY2DYQQGDsouCGAQSEGBouEKANl1onEwQvGA4QACA4IwQaIISFFwIwDXwI2ExL2BB4YABJgwwKnQAkRpzYDAAbuBA4oABe4aaDD44uGxCNEQwStEdwgAFR4IHGF4iRBxJeLBwIdCcwgvJxLwFFIRgLBo4dBcwj2CF45xIKIxeLAww4FAAuINIYbKMAteso8FAwovPCQllQQtlF4gLFUYwvDsq/HfIShEDhCQDIgIAFnQHGBBITRnWIXwdfAAYGFSYblBNI5KBsobEDo4GCdxyFDr2tR4+tDQrwNF5YlEnQvJaZIvKr4RIEoovaSAIvyXArbBF6OJR6mCXAgvBDwIlFd5IfBd6tfSQQRGCgeIBJE6DIIAFDoIGGF4OCAgIAEnQHGBBITRnWIF4P+V4ZMCWxBoBRw5PBNI7IGnQuCSAIfE1oSB1oADF4WIXxAvHsoIFH4IvEdIJWEDg4vICRTuJSAwABxAcJF5A5C1ovKRwhgCwSQGF6CpEXxBeGMA59JZIIvGA4hhBLw+JF4xRFSBBNDFIhHFe4VfLxhgCAEguIMAJSB1oABSA4HEB4RwBAgQXHss6LxKRDPQTxBsovIRIdexCOGRpwwIr5gFAwbuEAoheBFyQwFRIrwDLwZuBdwgTBC4IuRYYowGRAq+BAoeCAoRABFyIABD4IsCGAgpFGoguBd4eIFyRiEFoIwCEoKJDRwYqCCAJcUGJICBK4JgDKgOtOIQtce4s6AAI2EBAgteAAizBGgYECwQsiAD4"))
|
|
@ -0,0 +1,21 @@
|
|||
latLonToGridSquare=function(o,a){var t,e,n,s,l,i,r,h,M,f=-100,g=0,u="ABCDEFGHIJKLMNOPQRSTUVWX",d=u.toLowerCase();function N(o){return"number"==typeof o?o:"string"==typeof o?parseFloat(o):"function"==typeof o?parseFloat(o()):void E.showMessage("can't convert \ninput: "+o)}return"object"==typeof o?2===o.length?(f=N(o[0]),g=N(o[1])):"lat"in o&&"lon"in o?(f=N(o.lat),g=N(o.lon)):"latitude"in o&&"longitude"in o?(f=N(o.latitude),g=N(o.longitude)):E.showMessage("can't convert \nobject "+o):(f=N(o),g=N(a)),isNaN(f)&&E.showMessage("lat is NaN"),isNaN(g)&&E.showMessage("lon is NaN"),90===Math.abs(f)&&E.showMessage("grid invalid \nat N/S"),90<Math.abs(f)&&E.showMessage("invalid lat: \n"+f),180<Math.abs(g)&&E.showMessage("invalid lon: \n"+g),t=f+90,e=g+180,n=u[Math.floor(t/10)],s=u[Math.floor(e/20)],l=""+Math.floor(t%10),i=""+Math.floor(e/2%10),h=60*(t-Math.floor(t)),M=60*(e-2*Math.floor(e/2)),r=d[Math.floor(h/2.5)],s+n+i+l+d[Math.floor(M/5)]+r};
|
||||
|
||||
Bangle.setGPSPower(1);
|
||||
var fix;
|
||||
Bangle.removeAllListeners();
|
||||
Bangle.on('GPS',function(f) {
|
||||
fix=f;
|
||||
g.clear();
|
||||
g.setFontAlign(0,0);
|
||||
if (!f.fix) {
|
||||
g.setFont("6x8",3);
|
||||
g.drawString("Waiting for",120,70);
|
||||
g.drawString("GPS Fix",120,110);
|
||||
g.setFont("6x8",2);
|
||||
g.drawString(f.satellites+" satellites",120,170);
|
||||
} else {
|
||||
g.setFont("6x8",5);
|
||||
var maidenhead = latLonToGridSquare(fix.lat,fix.lon);
|
||||
g.drawString(maidenhead,120,120);
|
||||
}
|
||||
});
|
After Width: | Height: | Size: 3.6 KiB |
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
|
||||
0.02: Don't overwrite existing settings on app update
|
||||
Clean up recordings on app removal
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.02: Modified for use with new bootloader and firmware
|
||||
0.03: Added Locale based date
|
||||
0.04: Improve performance, attempt to remove occasional glitch when LCD on (fix #279)
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
var locale = require("locale");
|
||||
var CHARW = 34; // how tall are digits?
|
||||
var CHARP = 2; // how chunky are digits?
|
||||
var Y = 50; // start height
|
||||
// Offscreen buffer
|
||||
var buf = Graphics.createArrayBuffer(240,86,1,{msb:true});
|
||||
function flip() {
|
||||
g.setColor(1,1,1);
|
||||
g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},0,50);
|
||||
}
|
||||
var buf = Graphics.createArrayBuffer(CHARW+CHARP*2,CHARW*2 + CHARP*2,1,{msb:true});
|
||||
var bufimg = {width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer};
|
||||
// The last time that we displayed
|
||||
var lastTime = " ";
|
||||
// If animating, this is the interval's id
|
||||
var animInterval;
|
||||
var timeInterval;
|
||||
|
||||
/* Get array of lines from digit d to d+1.
|
||||
n is the amount (0..1)
|
||||
|
@ -49,7 +50,7 @@ const DIGITS = {
|
|||
[0,1,1,1],
|
||||
[1,1,1,2],
|
||||
[1-n,2,1,2]],
|
||||
"5": (n,maxFive)=>maxFive ? [ // 5 -> 0
|
||||
"5to0": n=>[ // 5 -> 0
|
||||
[0,0,0,1],
|
||||
[0,0,1,0],
|
||||
[n,1,1,1],
|
||||
|
@ -57,7 +58,8 @@ const DIGITS = {
|
|||
[0,2,1,2],
|
||||
[0,2,0,2],
|
||||
[1,1-n,1,1],
|
||||
[0,1,0,1+n]] : [ // 5 -> 6
|
||||
[0,1,0,1+n]],
|
||||
"5to6": n=>[ // 5 -> 6
|
||||
[0,0,0,1],
|
||||
[0,0,1,0],
|
||||
[0,1,1,1],
|
||||
|
@ -109,59 +111,66 @@ const DIGITS = {
|
|||
|
||||
/* Draw a transition between lastText and thisText.
|
||||
'n' is the amount - 0..1 */
|
||||
function draw(lastText,thisText,n) {
|
||||
buf.clear();
|
||||
var x = 1; // x offset
|
||||
const p = 2; // padding around digits
|
||||
var y = p; // y offset
|
||||
const s = 34; // character size
|
||||
function drawDigits(lastText,thisText,n) {
|
||||
const p = CHARP; // padding around digits
|
||||
const s = CHARW; // character size
|
||||
var x = 0; // x offset
|
||||
g.reset();
|
||||
for (var i=0;i<lastText.length;i++) {
|
||||
var lastCh = lastText[i];
|
||||
var thisCh = thisText[i];
|
||||
if (thisCh==":") x-=4;
|
||||
var ch, chn = n;
|
||||
if (lastCh!==undefined &&
|
||||
(thisCh-1==lastCh ||
|
||||
(thisCh==0 && lastCh==5) ||
|
||||
(thisCh==0 && lastCh==9)))
|
||||
ch = lastCh;
|
||||
else {
|
||||
ch = thisCh;
|
||||
chn = 0;
|
||||
if (lastCh!=thisCh) {
|
||||
var ch, chn = n;
|
||||
if ((thisCh-1==lastCh ||
|
||||
(thisCh==0 && lastCh==5) ||
|
||||
(thisCh==0 && lastCh==9)))
|
||||
ch = lastCh;
|
||||
else {
|
||||
ch = thisCh;
|
||||
chn = 0;
|
||||
}
|
||||
buf.clear();
|
||||
if (ch=="5") ch = (lastCh==5 && thisCh==0)?"5to0":"5to6";
|
||||
var l = DIGITS[ch](chn);
|
||||
l.forEach(c=>{
|
||||
if (c[0]!=c[2]) // horiz
|
||||
buf.fillRect(p+c[0]*s,c[1]*s,p+c[2]*s,2*p+c[3]*s);
|
||||
else if (c[1]!=c[3]) // vert
|
||||
buf.fillRect(c[0]*s,p+c[1]*s,2*p+c[2]*s,p+c[3]*s);
|
||||
});
|
||||
g.drawImage(bufimg,x,Y);
|
||||
}
|
||||
var l = DIGITS[ch](chn,lastCh==5 && thisCh==0);
|
||||
l.forEach(c=>{
|
||||
if (c[0]!=c[2]) // horiz
|
||||
buf.fillRect(x+c[0]*s,y+c[1]*s-p,x+c[2]*s,y+c[3]*s+p);
|
||||
else if (c[1]!=c[3]) // vert
|
||||
buf.fillRect(x+c[0]*s-p,y+c[1]*s,x+c[2]*s+p,y+c[3]*s);
|
||||
});
|
||||
if (thisCh==":") x-=4;
|
||||
x+=s+p+7;
|
||||
}
|
||||
y += 2*s;
|
||||
}
|
||||
function drawSeconds() {
|
||||
var x = (CHARW + CHARP + 6)*5;
|
||||
var y = Y + 2*CHARW + CHARP;
|
||||
var d = new Date();
|
||||
buf.setFont("6x8");
|
||||
buf.setFontAlign(-1,-1);
|
||||
buf.drawString(("0"+d.getSeconds()).substr(-2), x, y-8);
|
||||
g.reset();
|
||||
g.setFont("6x8");
|
||||
g.setFontAlign(-1,-1);
|
||||
g.drawString(("0"+d.getSeconds()).substr(-2), x, y-8, true);
|
||||
// date
|
||||
buf.setFontAlign(0,-1);
|
||||
g.setFontAlign(0,-1);
|
||||
var date = locale.date(d,false);
|
||||
buf.drawString(date, buf.getWidth()/2, y+8);
|
||||
flip();
|
||||
g.drawString(date, g.getWidth()/2, y+8, true);
|
||||
}
|
||||
|
||||
/* Show the current time, and animate if needed */
|
||||
function showTime() {
|
||||
if (!Bangle.isLCDOn()) return;
|
||||
if (animInterval) return; // in animation - quit
|
||||
var d = new Date();
|
||||
var t = (" "+d.getHours()).substr(-2)+":"+
|
||||
("0"+d.getMinutes()).substr(-2);
|
||||
var l = lastTime;
|
||||
// same - don't animate
|
||||
if (t==l) {
|
||||
draw(t,l,0);
|
||||
if (t==l || l==" ") {
|
||||
drawDigits(l,t,0);
|
||||
drawSeconds();
|
||||
lastTime = t;
|
||||
return;
|
||||
}
|
||||
var n = 0;
|
||||
|
@ -170,23 +179,35 @@ function showTime() {
|
|||
if (n>=1) {
|
||||
n=1;
|
||||
clearInterval(animInterval);
|
||||
animInterval=0;
|
||||
animInterval = undefined;
|
||||
}
|
||||
draw(l,t,n);
|
||||
drawDigits(l,t,n);
|
||||
}, 20);
|
||||
lastTime = t;
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower',function(on) {
|
||||
if (on)
|
||||
if (animInterval) {
|
||||
clearInterval(animInterval);
|
||||
animInterval = undefined;
|
||||
}
|
||||
if (timeInterval) {
|
||||
clearInterval(timeInterval);
|
||||
timeInterval = undefined;
|
||||
}
|
||||
if (on) {
|
||||
showTime();
|
||||
timeInterval = setInterval(showTime, 1000);
|
||||
} else {
|
||||
lastTime = " ";
|
||||
}
|
||||
});
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
// Update time once a second
|
||||
setInterval(showTime, 1000);
|
||||
timeInterval = setInterval(showTime, 1000);
|
||||
showTime();
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# Metronome
|
||||
|
||||
This metronome makes your watch blink and vibrate with a given rate.
|
||||
|
||||
## Usage
|
||||
|
||||
* Tap the screen at least three times. The app calculates the mean rate of your tapping. This rate is displayed in bmp while the text blinks and the watch softly vibrates with every beat.
|
||||
* Use `BTN1` to increase the bmp value by one.
|
||||
* Use `BTN3` to decrease the bmp value by one.
|
||||
* You can change the bpm value any time by tapping the screen or using `BTN1` and `BTN3`.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+ABt4AB4fOFyFOABtUGDotOAAYvcp4ARqovbq0rACAvbqwABF98yGCAvdGAcHgAAEF8tWmIuGGA6QaF4lWFw4vgFwovPmIvuYDIvd0ejF59cF6qQFFwIvnMAguSqxfaFyQvYvOi0QuTF64uCAAQuRXwIvUqouEF6guFF5+cAAiOZF6iOaF5sxv+iF6xfRmVWFwWjv8rp4tSL6YvBqwuDMgQvnFwovURwIvQRggAELygvPgwuIF8ouEBwIvnFwwwXF54uBvwuFq0yF6buCF5guClQuFGAgvfFwcAF49WmIvRFwQvKFwkAmQvHYQMxF7l+FwgvKGAIvalQuGF5dWFx1VABVUvF4p0qAAdPCZNPF51OAD4vOKQIACF/4waF9wuEqgv/F/gwMF97vvAAUqADYtQAAMAADYuRGDgmLA="))
|
|
@ -0,0 +1,93 @@
|
|||
var tStart = Date.now();
|
||||
var cindex=0; // index to iterate through colous
|
||||
var bpm=60; // ininital bpm value
|
||||
var time_diffs = [1000, 1000, 1000]; //array to calculate mean bpm
|
||||
var tindex=0; //index to iterate through time_diffs
|
||||
|
||||
Bangle.setLCDTimeout(undefined); //do not deaktivate display while running this app
|
||||
|
||||
function changecolor() {
|
||||
const maxColors = 2;
|
||||
const colors = {
|
||||
0: { value: 0xFFFF, name: "White" },
|
||||
1: { value: 0x000F, name: "Navy" },
|
||||
// 2: { value: 0x03E0, name: "DarkGreen" },
|
||||
// 3: { value: 0x03EF, name: "DarkCyan" },
|
||||
// 4: { value: 0x7800, name: "Maroon" },
|
||||
// 5: { value: 0x780F, name: "Purple" },
|
||||
// 6: { value: 0x7BE0, name: "Olive" },
|
||||
// 7: { value: 0xC618, name: "LightGray" },
|
||||
// 8: { value: 0x7BEF, name: "DarkGrey" },
|
||||
// 9: { value: 0x001F, name: "Blue" },
|
||||
// 10: { value: 0x07E0, name: "Green" },
|
||||
// 11: { value: 0x07FF, name: "Cyan" },
|
||||
// 12: { value: 0xF800, name: "Red" },
|
||||
// 13: { value: 0xF81F, name: "Magenta" },
|
||||
// 14: { value: 0xFFE0, name: "Yellow" },
|
||||
// 15: { value: 0xFFFF, name: "White" },
|
||||
// 16: { value: 0xFD20, name: "Orange" },
|
||||
// 17: { value: 0xAFE5, name: "GreenYellow" },
|
||||
// 18: { value: 0xF81F, name: "Pink" },
|
||||
};
|
||||
g.setColor(colors[cindex].value);
|
||||
if (cindex == maxColors-1) {
|
||||
cindex = 0;
|
||||
}
|
||||
else {
|
||||
cindex += 1;
|
||||
}
|
||||
return cindex;
|
||||
}
|
||||
|
||||
function updateScreen() {
|
||||
g.clear();
|
||||
changecolor();
|
||||
Bangle.buzz(50, 0.75);
|
||||
g.setFont("Vector",48);
|
||||
g.drawString(Math.floor(bpm)+"bpm", -1, 70);
|
||||
}
|
||||
|
||||
Bangle.on('touch', function(button) {
|
||||
// setting bpm by tapping the screen. Uses the mean time difference between several tappings.
|
||||
if (tindex < time_diffs.length) {
|
||||
if (Date.now()-tStart < 5000) {
|
||||
time_diffs[tindex] = Date.now()-tStart;
|
||||
}
|
||||
} else {
|
||||
tindex=0;
|
||||
time_diffs[tindex] = Date.now()-tStart;
|
||||
}
|
||||
tindex += 1;
|
||||
mean_time = 0.0;
|
||||
for(count = 0; count < time_diffs.length; count++) {
|
||||
mean_time += time_diffs[count];
|
||||
}
|
||||
time_diff = mean_time/count;
|
||||
|
||||
tStart = Date.now();
|
||||
clearInterval(time_diff);
|
||||
g.clear();
|
||||
g.setFont("Vector",48);
|
||||
bpm = (60 * 1000/(time_diff));
|
||||
g.drawString(Math.floor(bpm)+"bpm", -1, 70);
|
||||
clearInterval(interval);
|
||||
interval = setInterval(updateScreen, 60000 / bpm);
|
||||
return bpm;
|
||||
});
|
||||
|
||||
// enable bpm finetuning via buttons.
|
||||
setWatch(() => {
|
||||
bpm += 1;
|
||||
clearInterval(interval);
|
||||
interval = setInterval(updateScreen, 60000 / bpm);
|
||||
}, BTN1, {repeat:true});
|
||||
|
||||
setWatch(() => {
|
||||
if (bpm > 1) {
|
||||
bpm -= 1;
|
||||
clearInterval(interval);
|
||||
interval = setInterval(updateScreen, 60000 / bpm);
|
||||
}
|
||||
}, BTN3, {repeat:true});
|
||||
|
||||
interval = setInterval(updateScreen, 60000 / bpm);
|
After Width: | Height: | Size: 7.4 KiB |
|
@ -1 +1,2 @@
|
|||
0.01: First release
|
||||
0.02: Improved date readability, fixed drawing of widgets
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
const bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ulub7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBudJudPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNzAAIDGugGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMyHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1zzw0BDYI6B0R3DAAJ1BvMyp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw55CHwQABIQQBBABkzAILlCHQR1CFYavEPgsAAAIDEDQNdAwQAaHQNWEwQ0DHAh3KleBLoI7dHQKuFWQo0EAIsISoKdBHbyyHNgwADlVVpwEBDANWro7fd4Q6HO495vF5QgIYCd75eBHYUINAN5lQ3EA"));
|
||||
|
||||
const locale = require("locale");
|
||||
|
||||
const black = 0x0000;
|
||||
|
@ -9,6 +7,8 @@ let hour;
|
|||
let minute;
|
||||
let date;
|
||||
|
||||
let timer;
|
||||
|
||||
function draw() {
|
||||
const d = new Date();
|
||||
|
||||
|
@ -37,7 +37,7 @@ function draw() {
|
|||
}
|
||||
|
||||
if (newDate !== date) {
|
||||
g.setFontVector(12);
|
||||
g.setFont('6x8', 2);
|
||||
g.setColor(black);
|
||||
g.drawString(date, 120, 228);
|
||||
g.setColor(0xFFFF);
|
||||
|
@ -46,23 +46,32 @@ function draw() {
|
|||
}
|
||||
}
|
||||
|
||||
function drawAll() {
|
||||
function startDrawing() {
|
||||
hour = '';
|
||||
minute = '';
|
||||
date = '';
|
||||
var bob = require("heatshrink").decompress(atob("nk8hAaXlYLWAEsqvN/0gBBql5lQ2tquj1XV5wBJ52j0hACPsdP1QsBAQQAGBIIBF51/P8OkN5R1GIxF5HLmAFgoDLPZfOpzmZ6vPFwomCPaA6DAYOjeq2A1YyCdI4HGQJQ8F1T2SJ4Oq1XW1es1mtAQOrPoPUAIh3J54ZHIAR5S62s64cBwIBGQIOqHQK4HKQYVDAAIFC1g+BHh9VHAQAFDwQDDHoJ5E54BB6AaBKQ5YGqo6MwJzGHQ4BDeIj/BR4JxDABY8BvI6OOYgaEHwZADHgQ6BZA42GAIusPJNW64eFqzJDlcrERA8BHQI2FqwaBDYYGBPI45GCoIgCLoVWQ5NWXA2rKhaiGLAwOGEAmADxJPDVA51ElQaMC4ouEWALdEHRg8Dc4woCDJo8EAIYxCHQQIFHiwaRegJ5EcYWsHgbrKbBA8GDSrNDO4wfRKgR3FDSh3CN4UrdwZbSHYZ5DHajMFHYQGCHalWO4jtQDQwABwAGCAAQfTKoK0EHahwCeARdFHakASIZWVZ4Q8CO4YgWO4QbCO6hWGEIKYZKzZ3DLog7UG4I6C1lWDSdWO4bpCO4bwUwKYEHajMDwOAlUkLojTUd4gaTZoRWC0YIB1eJLqo4EWiqRE0mjlcr1QkEeKFWOooBCHiB2CC4WA5wzB52rEQgfPHQwABAYJXOHQ2iO4XO6omFEJh1BEAgBGPJlWDIQbC0ej50qgHV1XPEwohKcwRbEvJ3EBQTrLFomkOwOjlR3C5w8GMAR8ClYuBLIgOCvN4HgIZFDQYbBlaOBR4YNCwA5B0XOpzvBHgWqTw4AFxB1EvQ6BAAI8GDZILEdgQBCqp3DPIRfIEQwABvJ1CvGkvGiwA6IAYoBCv6wCAAVOlQ6DAIWkL5ABEwN40Z1CAYJfCv7zEAJNWOYZ3KAIWq5yYHFYLOBLIrVCAIh7BOIzpECoYDDpw7BHAQDG1WqwGkAIN/CwIABLY4LDAAZ9BwAABvLCBC5IBBvOrO44BEAAmjAIPN0XOAJyHIAAgPEquBHBi4BAId+HwWiHxqJDRpYBDq2I1R3LIAQ6BAAOpPogABGgIDDOomk0nP5pIGd42Aq1ewI8CeZI6CHAJhCEAIDCdIo2B0er1esAQIZBC4Z9JlVW1leeKGp0es5+s6+s6ABB0oFBAIervWr0ultr7OqtWmdexJ4BGxB5G0V6pF6wItB0t6p9PvQABvINBttJttPzwXCHQwBDlY6BO4WIwPPPJbvDvA0BFgNtAAIDGtwGCzrtHdYh1DAIOBrzxBPIgAIeIXNHgoBCGwYADzoVB0fNOpMOHQdW5+Bro7BHgQAB6jxJAAOjOYhxCAIukOoPN5ujdZFWqyyD0d6AwUrquBwAuB1I1FHgRJBMoQ9BWg1tzw0BDYI6B0R3DAAJ1BvMOp8rAAV4IQWBIodewAeCHAZ5IAAJoBAAXHHAJWDO4TtEdYQvEHgejAwIKClcqIQRdDXYbzFeoQBIGwIDDOot/VgQ6FAAIGBlgBCAAMzmZPBF4LzDACB1FAAOi1WjvFVr0zGYQ7GAAMAAYpPBwNeAIOIwOrfYOA1eA1WkAIWjAIekv4PBwCVBruBq5eBEYIABlcBF4wCEHw8zgAAiFYivEPgoSEAYo9jGgY4EO5Q7kVwiyFGggBFhASBHkhsKAAcqqtOAgMzd8o6HO495vF5QgMzrw7lhBoBvMqG4g"));
|
||||
g.drawImage(bob, 0, 0, { scale: 4 });
|
||||
Bangle.drawWidgets();
|
||||
draw();
|
||||
timer = setInterval(draw, 1000);
|
||||
}
|
||||
|
||||
function stopDrawing() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Bangle.on('lcdPower', function(on) {
|
||||
stopDrawing();
|
||||
if (on) {
|
||||
drawAll();
|
||||
startDrawing();
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
setInterval(draw, 1000);
|
||||
drawAll();
|
||||
startDrawing();
|
||||
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
|
||||
setWatch(Bangle.showLauncher, BTN2, { repeat: false, edge: 'falling' });
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
0.04: Run again when updated
|
||||
Don't run again when settings app is updated (or absent)
|
||||
Add "Run Now" option to settings
|
||||
0.05: Don't overwrite existing settings on app update
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
(function() {
|
||||
let s = require('Storage').readJSON('ncstart.settings.json', 1)
|
||||
let s = require('Storage').readJSON('ncstart.json', 1)
|
||||
|| require('Storage').readJSON('setting.json', 1)
|
||||
|| {welcomed: true} // do NOT run if global settings are also absent
|
||||
if (!s.welcomed && require('Storage').read('ncstart.app.js')) {
|
||||
setTimeout(() => {
|
||||
s.welcomed = true
|
||||
require('Storage').write('ncstart.settings.json', s)
|
||||
require('Storage').write('ncstart.json', s)
|
||||
load('ncstart.app.js')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"welcomed": false
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
// The welcome app is special, and gets to use global settings
|
||||
(function(back) {
|
||||
let settings = require('Storage').readJSON('ncstart.settings.json', 1)
|
||||
let settings = require('Storage').readJSON('ncstart.json', 1)
|
||||
|| require('Storage').readJSON('setting.json', 1) || {}
|
||||
E.showMenu({
|
||||
'': { 'title': 'NCEU Startup' },
|
||||
'Run on Next Boot': {
|
||||
value: !settings.welcomed,
|
||||
format: v => v ? 'OK' : 'No',
|
||||
onchange: v => require('Storage').write('ncstart.settings.json', {welcomed: !v}),
|
||||
onchange: v => require('Storage').write('ncstart.json', {welcomed: !v}),
|
||||
},
|
||||
'Run Now': () => load('ncstart.app.js'),
|
||||
'< Back': back,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Use BTN2 for settings menu like other clocks
|
||||
0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting
|
||||
0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting
|
||||
0.04: Don't overwrite existing settings on app update
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
color:0,
|
||||
drawMode:"fill",
|
||||
menuButton:22
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New App!
|
||||
0.02: Change img when no fix
|
|
@ -0,0 +1,5 @@
|
|||
# Points Of Interest Compass
|
||||
|
||||
## Description
|
||||
|
||||
Uploads all the points of interest in an area onto your watch, same as Beer Compass with more p.o.i.
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,228 @@
|
|||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="../../css/spectre.min.css">
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html, body, #map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
#map { z-index: 1; }
|
||||
#controls {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border: 1px solid black;
|
||||
position:absolute;
|
||||
right:0px;top:0px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map">
|
||||
</div>
|
||||
|
||||
<div id="controls">
|
||||
<select id="amenity">
|
||||
<option value="drinking_water">Drinking Water</option>
|
||||
<option value="bicycle_parking">Bicycle Park</option>
|
||||
<option value="post_box">Post Box</option>
|
||||
<option value="bar">Bars</option>
|
||||
<option value="bbq">Barbecue</option>
|
||||
<option value="cafe">Café</option>
|
||||
<option value="fast_food">Fast food</option>
|
||||
<option value="restaurant">restaurant</option>
|
||||
<option value="bicycle_repair_station">Bicycle Repair</option>
|
||||
<option value="bicycle_rental">Bicycle rental</option>
|
||||
<option value="bus_station">Bus station</option>
|
||||
<option value="atm">Bank</option>
|
||||
<option value="pharmacy">Pharmacy</option>
|
||||
<option value="interner_cafe">Cybercafé</option>
|
||||
</select>
|
||||
<p>Click <button id="poi" class="btn btn-primary">Find P.O.I.</button></p>
|
||||
<p>If ok, Click <button id="upload" class="btn btn-primary">Upload</button></p>
|
||||
</div>
|
||||
|
||||
<script src="../../lib/customize.js"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.0.3/dist/leaflet.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
|
||||
<script src="https://unpkg.com/osmtogeojson@2.2.12/osmtogeojson.js"></script>
|
||||
|
||||
<script>
|
||||
/*
|
||||
|
||||
https://overpass-turbo.eu/ to check queries.
|
||||
Run:
|
||||
|
||||
node
|
||||
({{bbox}});
|
||||
out;
|
||||
|
||||
Over the area you're interested in, click on the POI and find out
|
||||
what you need to search for, eg.
|
||||
|
||||
node
|
||||
[amenity=pub]
|
||||
({{bbox}});
|
||||
out;
|
||||
|
||||
*/
|
||||
|
||||
var map = L.map('map').locate({setView: true, maxZoom: 16});
|
||||
var features = [];
|
||||
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 18,
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
|
||||
}).addTo(map);
|
||||
function buildOverpassApiUrl(map, overpassQuery) {
|
||||
var bounds = map.getBounds().getSouth() + ',' + map.getBounds().getWest() + ',' + map.getBounds().getNorth() + ',' + map.getBounds().getEast();
|
||||
var nodeQuery = 'node[' + overpassQuery + '](' + bounds + ');';
|
||||
var wayQuery = 'way[' + overpassQuery + '](' + bounds + ');';
|
||||
var relationQuery = 'relation[' + overpassQuery + '](' + bounds + ');';
|
||||
var query = '?data=[out:json][timeout:15];(' + nodeQuery + wayQuery + relationQuery + ');out body geom;';
|
||||
var baseUrl = 'https://overpass-api.de/api/interpreter';
|
||||
var resultUrl = baseUrl + query;
|
||||
return resultUrl;
|
||||
}
|
||||
|
||||
document.getElementById("poi").addEventListener("click", function() {
|
||||
//var overpassApiUrl = buildOverpassApiUrl(map, "highway=bus_stop");
|
||||
var poi = document.getElementById("amenity").options[document.getElementById("amenity").selectedIndex].value;
|
||||
var overpassApiUrl = buildOverpassApiUrl(map, "amenity="+poi);
|
||||
var req = new XMLHttpRequest();
|
||||
req.responseType = 'json';
|
||||
req.onreadystatechange = function() {
|
||||
console.log("XMLHttpRequest", this.readyState);
|
||||
if (this.readyState == 4) {
|
||||
var osmDataAsJson = req.response;
|
||||
features = [];
|
||||
if (osmDataAsJson.elements) {
|
||||
osmDataAsJson.elements.forEach(function(el) {
|
||||
if (el.type!="node") return;
|
||||
features.push({lat:el.lat, lon:el.lon,name:el.tags.name});
|
||||
});
|
||||
}
|
||||
|
||||
var resultAsGeojson = osmtogeojson(osmDataAsJson);
|
||||
var resultLayer = L.geoJson(resultAsGeojson, {
|
||||
style: function (feature) {
|
||||
return {color: "#ff0000"};
|
||||
},
|
||||
filter: function (feature, layer) {
|
||||
var isPolygon = (feature.geometry) && (feature.geometry.type !== undefined) && (feature.geometry.type === "Polygon");
|
||||
if (isPolygon) {
|
||||
feature.geometry.type = "Point";
|
||||
var polygonCenter = L.latLngBounds(feature.geometry.coordinates[0]).getCenter();
|
||||
feature.geometry.coordinates = [ polygonCenter.lat, polygonCenter.lng ];
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onEachFeature: function (feature, layer) {
|
||||
var popupContent = "";
|
||||
popupContent = popupContent + "<dt>@id</dt><dd>" + feature.properties.type + "/" + feature.properties.id + "</dd>";
|
||||
var keys = Object.keys(feature.properties.tags);
|
||||
keys.forEach(function (key) {
|
||||
popupContent = popupContent + "<dt>" + key + "</dt><dd>" + feature.properties.tags[key] + "</dd>";
|
||||
});
|
||||
popupContent = popupContent + "</dl>"
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
}).addTo(map);
|
||||
}
|
||||
};
|
||||
req.open("GET", overpassApiUrl, true);
|
||||
req.send();
|
||||
});
|
||||
document.getElementById("upload").addEventListener("click", function() {
|
||||
var app = `var features = ${JSON.stringify(features)};
|
||||
var img_nofix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AH4A/AC2swMllcrBQoHBktWwIEBlYEBqwAEgAICF6FWldbrgDBAIMrFIIhBrlbAAIIBwOsAAgsClcxF6ErrmG2GGw1cqsxrmAwuG2OxxkxFoOCq1VFANVLgIyBPI4vLq2F2OwAAQrCAgPVw0xEgVVMoQ7BwsxmOm1iUCMCIuD2GM2AvC2IuDFQIACwADCrZrCGCOBqovI2NcEAIuBTwQAHlZhCL6FVYAKQG2NV1i3BZYIpDrZiFlb1BYSFWwAvG6tbwOCmIuFF4YAC2FVwOBMCFWraBCRwReDBYIpELQOAAQQ5CqpgBF57RBFYK+C2IcBw4vBEQIwDrYEDrlbGYOMmMr64vQd4hcBX4VcF4LsGOIYGCxhMBYAKORGAhhBF4TtBAAzvFwuAwIvPlbuBXIQAE2BMBHgIoBE4QvEricBqtWF4KPOF4KICAAReCd4WBdoYAJmKPCF50lrhYCYIuxreBwUxR5RrBrmCwIABF54pCFwrkBJoMxFAKGBF4lcFwNbR4JxBX6GGFYeHSoOGSQJgBGARVBFoIqCMIWMlS+ClbvRMIbFEJ4PXwMrmIqBAARjDleB67ABFxowCYAgDBQQVbFwIAB00xGAQMDA4OCNoJdOAAWBYAex6uGNAIcBFwSvB1mCq1VFYMxqwICA4IvRDgKKDw0qwQrBPgIuBX4IoCAAmBDILQCF6AwBwBgBFwgwCldVTQKJBqtcwB1BSgXV6uAXyAvCrYuHAAMrrYuBGIQABwoGBOgWxF6SEBFwWmFwpgBF4IADxgyC2IFCR6Z5BLpHX1krLoQADLYQHEqrvRWQMrFxKOEF4eMxgyBGgVWkovRgCMH1kqZIInBLwwtCRwZfQqxdLEYQuFAQYHCraORq2BXZPVKwIABEoIGBFoYCCRwNWRyIuIquGrgfBldWwGAwqMBFYVbrlcqovSL42sksrwCsBD4QDBkoyBqsxFQINBmItRL4UrGAZdBLIIABCQ8lgAsBH4IsSGA4uBDq4wTq0xmIuqAH4A/AH4A/AH4AHA="));
|
||||
var img_fix = require("heatshrink").decompress(atob("mEwhBC/AH4A/AH4A/AE0rg4vtq8PF1kHxM7gJesF4Jgrg+IF4M6MFReBF4JgqXoIvDMFJeCF4RgoLwYvDnZgmLwYvEMEpeEF4jBlmYvIMEheFF4pgjXogvGMEReGF407MEBeGF45gfg+IF5rBfLw4vHMDxeIF5BgdLxAvIMDkHFxAvJMDZeJF5JgaLxQvKnZgYLxQvLMC5eLF5bBXmYvWMCxeMF5hgVXpYvNMCheNF5s7MCReNF5xgRg+IFZNersylcHhEPmjBbLw1er1XlkHIhI0CnRgUmQqEKwKnRgIzBiBpBnUICpszryCCFiIyGM4TBRAH4A/AH4A/AH4AFA"));
|
||||
|
||||
// https://github.com/Leaflet/Leaflet/blob/master/src/geo/projection/Projection.SphericalMercator.js
|
||||
function project(latlong) {
|
||||
var d = Math.PI / 180,
|
||||
max = 85.0511287798,
|
||||
R = 6378137, // earth radius in m
|
||||
lat = Math.max(Math.min(max, latlong.lat), -max),
|
||||
sin = Math.sin(lat * d);
|
||||
return {x:R * latlong.lon * d,
|
||||
y:R * Math.log((1 + sin) / (1 - sin)) / 2};
|
||||
}
|
||||
|
||||
features.forEach(function(f) {
|
||||
f.p = project(f);
|
||||
});
|
||||
var fix = {fix:false,satellites:0};
|
||||
var nearest = undefined;
|
||||
var nearestangle = 0;
|
||||
var nearestdist = 0;
|
||||
|
||||
Bangle.on('GPS', function(f) {
|
||||
fix = f;
|
||||
fix.p = project(fix);
|
||||
nearest = undefined;
|
||||
nearestangle = 0;
|
||||
nearestdist = 5000; // 5km
|
||||
features.forEach(function(f) {
|
||||
var dx = f.p.x - fix.p.x;
|
||||
var dy = f.p.y - fix.p.y;
|
||||
var d = Math.sqrt(dx*dx + dy*dy);
|
||||
if (d<nearestdist) {
|
||||
nearestdist = d;
|
||||
nearest = f;
|
||||
}
|
||||
});
|
||||
if (nearest) {
|
||||
var dx = nearest.p.x - fix.p.x;
|
||||
var dy = nearest.p.y - fix.p.y;
|
||||
nearestangle = Math.atan2(dy,dx);
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.on('mag', function(m) {
|
||||
if (!Bangle.isLCDOn()) return;
|
||||
var headingrad = m.heading*Math.PI/180; // in radians
|
||||
if (!isFinite(headingrad)) headingrad=0;
|
||||
if (nearest)
|
||||
g.drawImage(img_fix,120,120,{
|
||||
rotate: (Math.PI/2)+headingrad-nearestangle,
|
||||
scale:3,
|
||||
});
|
||||
else
|
||||
g.drawImage(img_nofix,120,120,{
|
||||
rotate: headingrad,
|
||||
scale:2,
|
||||
});
|
||||
g.clearRect(60,0,180,24);
|
||||
g.setFontAlign(0,0);
|
||||
g.setFont("6x8");
|
||||
if (fix.fix) {
|
||||
g.drawString(nearest ? nearest.name : "---",120,4);
|
||||
g.setFont("6x8",2);
|
||||
g.drawString(nearest ? Math.round(nearestdist)+"m" : "---",120,16);
|
||||
} else {
|
||||
g.drawString(fix.satellites+" satellites",120,4);
|
||||
}
|
||||
});
|
||||
Bangle.setCompassPower(1);
|
||||
Bangle.setGPSPower(1);
|
||||
g.clear();`;
|
||||
var icon = `require("heatshrink").decompress(atob("mEwxH+AH4A0PgYurg9kr0rGM4nBg9dsgADmUHGUYtHAAddGIJcgFpIxEMTsAlYtMAAZiaLh4AFmQwXLiSTaLiosBnMymUrGCYTBAAgvPCgjwaMh4raF/4v/F/4vUg4vulZgDgAAIF8EyEQUAh0cAAkVisHGA4+HF6gVBiwwFjkONo0HAAMOAAIvTnIhEiovMFQQADNgYvQroUDg4uGjj9EF448DF6a+HAAS+EF5CQCF6SMIeAQvFXYYwHF59eLpSOBF4hgKGAIvPskAFxKOFF80VM4KOFSBs5F6EWRY4xBF43+F5UyF6DuEizZBKwIuHSBQvXdIwvKMYsHlYvQW4IuPYAYRBMggvRgIvCFxwwCTYZlEg4vPlcHjkVFx6WJF6YuXMoTwCF58yVgIvXY4YvQrqqDGDAvvPYIvPsguaYQYv/F7owBF/4vfg4AGTIIAHF7gA/AH4AwA="))`;
|
||||
|
||||
sendCustomizedApp({
|
||||
storage:[
|
||||
{name:"osmpoi.app.js", content:app},
|
||||
{name:"osmpoi.img", content:icon, evaluate:true},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgIEBgOABQYFD8AUEApoXFDqIXV4BYGKZIANsIRE+IFE/IFEvCIFGrgXLDqIAOgc/9/2hv+g8///3AoUwvE3xuABYP4m3NzwFB7E2tu/CIMYm09wYFDjoFCj4pB/8HkEP+EBFII7EAosDJxYA="))
|
|
@ -0,0 +1,282 @@
|
|||
/**
|
||||
* BangleJS Pong game
|
||||
*
|
||||
* Original Author: Frederic Rousseau https://github.com/fredericrous
|
||||
* Created: April 2020
|
||||
*
|
||||
* Inspired by:
|
||||
* - Let's make pong, One Man Army Studios, Youtube
|
||||
* - Pong.js, KanoComputing, Github
|
||||
* - Coding Challenge #67: Pong!, The Coding Train, Youtube
|
||||
*/
|
||||
|
||||
const SCREEN_WIDTH = 240;
|
||||
const FPS = 16;
|
||||
const MAX_SCORE = 11;
|
||||
let scores = [0, 0];
|
||||
let aiSpeedRandom = 0;
|
||||
|
||||
function Vector(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
Vector.prototype.add = function (x) {
|
||||
this.x += x.x || 0;
|
||||
this.y += x.y || 0;
|
||||
return this;
|
||||
};
|
||||
|
||||
const constrain = (n, low, high) => Math.max(Math.min(n, high), low);
|
||||
const random = (min, max) => Math.random() * (max - min) + min;
|
||||
const intersects = (circ, rect) => {
|
||||
var c1 = circ.pos, c2 = {x: circ.pos.x+circ.r, y: circ.pos.y+circ.r};
|
||||
var r1 = rect.pos, r2 = {x: rect.pos.x+rect.width*2, y: rect.pos.y+rect.height};
|
||||
return !(c1.x > r2.x || c2.x < r1.x ||
|
||||
c1.y > r2.y || c2.y < r1.y);
|
||||
};
|
||||
|
||||
///////////////////////////// Ball //////////////////////////////////////////
|
||||
|
||||
function Ball() {
|
||||
this.r = 4;
|
||||
this.prevPos = null;
|
||||
this.originalSpeed = 4;
|
||||
this.maxSpeed = 6;
|
||||
|
||||
this.reset();
|
||||
}
|
||||
Ball.prototype.show = function () {
|
||||
if (this.prevPos != null) {
|
||||
g.setColor(0);
|
||||
g.fillCircle(this.prevPos.x, this.prevPos.y, this.prevPos.r);
|
||||
}
|
||||
g.setColor(-1);
|
||||
g.fillCircle(this.pos.x, this.pos.y, this.r);
|
||||
this.prevPos = {
|
||||
x: this.pos.x,
|
||||
y: this.pos.y,
|
||||
r: this.r
|
||||
};
|
||||
};
|
||||
Ball.prototype.bouncePlayer = function (multiplyX, multiplyY, player) {
|
||||
this.speed = constrain(this.speed + 2, this.originalSpeed, this.maxSpeed);
|
||||
var relativeIntersectY = (player.pos.y+(player.height/2)) - this.pos.y;
|
||||
var normalizedRelativeIntersectionY = (relativeIntersectY/(player.height/2));
|
||||
var MAX_BOUNCE_ANGLE = 4 * Math.PI/12;
|
||||
var bounceAngle = normalizedRelativeIntersectionY * MAX_BOUNCE_ANGLE;
|
||||
this.velocity.x = this.speed * Math.cos(bounceAngle) * multiplyX;
|
||||
this.velocity.y = this.speed * -Math.sin(bounceAngle) * multiplyY;
|
||||
};
|
||||
Ball.prototype.bounce = function (multiplyX, multiplyY, player) {
|
||||
if (player)
|
||||
return this.bouncePlayer(multiplyX, multiplyY, player);
|
||||
|
||||
if (multiplyX) {
|
||||
this.velocity.x = Math.abs(this.velocity.x) * multiplyX;
|
||||
}
|
||||
if (multiplyY) {
|
||||
this.velocity.y = Math.abs(this.velocity.y) * multiplyY;
|
||||
}
|
||||
};
|
||||
Ball.prototype.checkWallsCollision = function () {
|
||||
if (this.pos.y < 0) {
|
||||
this.bounce(0, 1);
|
||||
} else if (this.pos.y > SCREEN_WIDTH) {
|
||||
this.bounce(0, -1);
|
||||
} else if (this.pos.x < 0) {
|
||||
scores[1]++;
|
||||
if (scores[1] >= MAX_SCORE) {
|
||||
this.restart();
|
||||
state = 3;
|
||||
winnerMessage = "AI Wins!";
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
} else if (this.pos.x > SCREEN_WIDTH) {
|
||||
scores[0]++;
|
||||
if (scores[0] >= MAX_SCORE) {
|
||||
this.restart();
|
||||
state = 3;
|
||||
winnerMessage = "You Win!";
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
Ball.prototype.checkPlayerCollision = function (player) {
|
||||
if (intersects(this, player)) {
|
||||
if (this.pos.x < SCREEN_WIDTH/2) {
|
||||
this.bounce(1, 1, player);
|
||||
this.pos.add(new Vector(this.width, 0));
|
||||
aiSpeedRandom = random(-1.6, 1.6);
|
||||
} else {
|
||||
this.bounce(-1, 1, player);
|
||||
this.pos.add(new Vector(-(this.width / 2 + 1), 0));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Ball.prototype.checkCollisions = function () {
|
||||
return this.checkWallsCollision() || this.checkPlayerCollision(player) || this.checkPlayerCollision(ai);
|
||||
};
|
||||
Ball.prototype.updatePosition = function () {
|
||||
var elapsed = new Date().getTime() - this.lastUpdate;
|
||||
var x = (elapsed / 50) * this.velocity.x;
|
||||
var y = (elapsed / 50) * this.velocity.y;
|
||||
this.pos.add(new Vector(x, y));
|
||||
};
|
||||
Ball.prototype.update = function () {
|
||||
this.updatePosition();
|
||||
this.lastUpdate = new Date().getTime();
|
||||
this.checkCollisions();
|
||||
};
|
||||
Ball.prototype.reset = function() {
|
||||
this.speed = this.originalSpeed;
|
||||
var x = scores[0] < scores[1] || (scores[0] === 0 && scores[1] === 0) ? -this.speed : this.speed;
|
||||
var bounceAngle = Math.PI/6;
|
||||
this.velocity = new Vector(x * Math.cos(bounceAngle), this.speed * -Math.sin(bounceAngle));
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, random(0, SCREEN_WIDTH));
|
||||
};
|
||||
Ball.prototype.restart = function() {
|
||||
ai.pos = new Vector(SCREEN_WIDTH - ai.width*2, SCREEN_WIDTH/2 - ai.height/2);
|
||||
player.pos = new Vector(player.width*2, SCREEN_WIDTH/2 - player.height/2);
|
||||
this.pos = new Vector(SCREEN_WIDTH/2, SCREEN_WIDTH/2);
|
||||
};
|
||||
|
||||
//////////////////////////// Player /////////////////////////////////////////
|
||||
|
||||
function Player() {
|
||||
this.width = 4;
|
||||
this.height = 30;
|
||||
this.pos = new Vector(this.width*2, SCREEN_WIDTH/2 - this.height/2);
|
||||
this.acc = new Vector(0, 0);
|
||||
this.speed = 15;
|
||||
this.maxSpeed = 25;
|
||||
this.prevPos = null;
|
||||
}
|
||||
Player.prototype.show = function () {
|
||||
if (this.prevPos != null) {
|
||||
g.setColor(0);
|
||||
g.fillRect(this.prevPos.x1, this.prevPos.y1, this.prevPos.x2, this.prevPos.y2);
|
||||
}
|
||||
g.setColor(-1);
|
||||
g.fillRect(this.pos.x, this.pos.y, this.pos.x+this.width, this.pos.y+this.height);
|
||||
this.prevPos = {
|
||||
x1: this.pos.x,
|
||||
y1: this.pos.y,
|
||||
x2: this.pos.x+this.width,
|
||||
y2: this.pos.y+this.height
|
||||
};
|
||||
};
|
||||
Player.prototype.up = function () {
|
||||
this.acc.y -= this.speed;
|
||||
};
|
||||
Player.prototype.down = function () {
|
||||
this.acc.y += this.speed;
|
||||
};
|
||||
Player.prototype.stop = function () {
|
||||
this.acc.y = 0;
|
||||
};
|
||||
Player.prototype.update = function () {
|
||||
this.acc.y = constrain(this.acc.y, -this.maxSpeed, this.maxSpeed);
|
||||
this.pos.add(this.acc);
|
||||
this.pos.y = constrain(this.pos.y, 0, SCREEN_WIDTH-this.height);
|
||||
};
|
||||
|
||||
////////////////////////////// AI ///////////////////////////////////////////
|
||||
|
||||
function AI() {
|
||||
Player.call(this);
|
||||
this.pos = new Vector(SCREEN_WIDTH-this.width*2, SCREEN_WIDTH/2 - this.height/2);
|
||||
}
|
||||
AI.prototype = Object.create(Player.prototype);
|
||||
AI.prototype.constructor = Player;
|
||||
AI.prototype.update = function () {
|
||||
var y = ball.pos.y - (this.height/2 * aiSpeedRandom);
|
||||
var yConstrained = constrain(y, 0, SCREEN_WIDTH-this.height);
|
||||
this.pos = new Vector(this.pos.x, yConstrained);
|
||||
};
|
||||
|
||||
function net() {
|
||||
var dashSize = 5;
|
||||
for (let y = dashSize/2; y < SCREEN_WIDTH; y += dashSize*2) {
|
||||
g.setColor(-1);
|
||||
let halfScreen = SCREEN_WIDTH/2;
|
||||
g.fillRect(halfScreen-dashSize/2, y, halfScreen+dashSize/2, y+dashSize);
|
||||
}
|
||||
}
|
||||
|
||||
var player = new Player();
|
||||
var ai = new AI();
|
||||
var ball = new Ball();
|
||||
var state = 0;
|
||||
var prevScores = [0, 0];
|
||||
|
||||
function drawScores() {
|
||||
let x1 = SCREEN_WIDTH/4-5;
|
||||
let x2 = SCREEN_WIDTH*3/4-5;
|
||||
|
||||
g.setColor(0);
|
||||
g.setFont('Vector', 20);
|
||||
g.drawString(prevScores[0], x1, 7);
|
||||
g.drawString(prevScores[1], x2, 7);
|
||||
g.setColor(-1);
|
||||
g.setFont('Vector', 20);
|
||||
g.drawString(scores[0], x1, 7);
|
||||
g.drawString(scores[1], x2, 7);
|
||||
prevScores = scores.slice();
|
||||
}
|
||||
|
||||
function drawGameOver() {
|
||||
g.setFont("Vector", 20);
|
||||
g.drawString(winnerMessage, 75, SCREEN_WIDTH/2 - 10);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (state === 1) {
|
||||
ball.update();
|
||||
player.update();
|
||||
ai.update();
|
||||
ball.show();
|
||||
player.show();
|
||||
ai.show();
|
||||
net();
|
||||
ball.show();
|
||||
} else if (state === 3) {
|
||||
g.clear();
|
||||
g.setColor(0);
|
||||
g.fillRect(0,0,240,240);
|
||||
state++;
|
||||
} else if (state === 4) {
|
||||
drawGameOver();
|
||||
} else {
|
||||
player.show();
|
||||
ai.show();
|
||||
net();
|
||||
}
|
||||
drawScores();
|
||||
}
|
||||
|
||||
g.clear();
|
||||
g.setColor(0);
|
||||
g.fillRect(0,0,240,240);
|
||||
|
||||
setInterval(draw, 1000 / FPS);
|
||||
|
||||
setWatch(o => o.state ? player.up() : player.stop(), BTN1, {repeat: true, edge: 'both'});
|
||||
setWatch(o => o.state ? player.down() : player.stop(), BTN3, {repeat: true, edge: 'both'});
|
||||
//setWatch(o => o.state ? player.down() : player.stop(), BTN5, {repeat: true, edge: 'both'});
|
||||
setWatch(o => {
|
||||
state++;
|
||||
if (state >= 2) {
|
||||
ball.restart();
|
||||
g.setColor(0);
|
||||
g.fillRect(0,0,240,240);
|
||||
scores = [0, 0];
|
||||
state = 1;
|
||||
}
|
||||
}, BTN2, {repeat: true});
|
After Width: | Height: | Size: 894 B |
|
@ -19,3 +19,4 @@
|
|||
0.15: Reduce memory usage when running default clock chooser (#294)
|
||||
0.16: Reduce memory usage further when running app settings page
|
||||
0.17: Remove need for "settings" in appid.info
|
||||
0.18: Don't overwrite existing settings on app update
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
ble: true, // Bluetooth enabled by default
|
||||
blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used?
|
||||
log: false, // Do log messages appear on screen?
|
||||
timeout: 10, // Default LCD timeout in seconds
|
||||
vibrate: true, // Vibration enabled by default. App must support
|
||||
beep: "vib", // Beep enabled by default. App must support
|
||||
timezone: 0, // Set the timezone for the device
|
||||
HID : false, // BLE HID mode, off by default
|
||||
clock: null, // a string for the default clock's name
|
||||
"12hour" : false, // 12 or 24 hour clock?
|
||||
// welcomed : undefined/true (whether welcome app should show)
|
||||
brightness: 1, // LCD brightness from 0 to 1
|
||||
options: {
|
||||
wakeOnBTN1: true,
|
||||
wakeOnBTN2: true,
|
||||
wakeOnBTN3: true,
|
||||
wakeOnFaceUp: false,
|
||||
wakeOnTouch: false,
|
||||
wakeOnTwist: true,
|
||||
twistThreshold: 819.2,
|
||||
twistMaxY: -800,
|
||||
twistTimeout: 1000
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
(() => {
|
||||
function draw(w) {
|
||||
g.reset();
|
||||
g.setColor(0).fillRect(0, 24, 239, 239);
|
||||
|
||||
require('weather').drawIcon(w.txt, 65, 90, 55);
|
||||
const locale = require("locale");
|
||||
|
||||
g.setColor(-1);
|
||||
|
||||
const temp = locale.temp(w.temp-273.15).match(/^(\D*\d*)(.*)$/);
|
||||
let width = g.setFont("Vector", 40).stringWidth(temp[1]);
|
||||
width += g.setFont("Vector", 20).stringWidth(temp[2]);
|
||||
g.setFont("Vector", 40).setFontAlign(-1, -1, 0);
|
||||
g.drawString(temp[1], 180-width/2, 70);
|
||||
g.setFont("Vector", 20).setFontAlign(1, -1, 0);
|
||||
g.drawString(temp[2], 180+width/2, 70);
|
||||
|
||||
g.setFont("6x8", 1);
|
||||
g.setFontAlign(-1, 0, 0);
|
||||
g.drawString("Humidity", 135, 130);
|
||||
g.drawString("Wind", 135, 142);
|
||||
g.setFontAlign(1, 0, 0);
|
||||
g.drawString(w.hum+"%", 225, 130);
|
||||
g.drawString(locale.speed(w.wind), 225, 142);
|
||||
|
||||
g.setFont("6x8", 2).setFontAlign(0, 0, 0);
|
||||
g.drawString(w.loc, 120, 170);
|
||||
|
||||
g.setFont("6x8", 1).setFontAlign(0, 0, 0);
|
||||
g.drawString(w.txt.charAt(0).toUpperCase()+w.txt.slice(1), 120, 190);
|
||||
|
||||
g.flip();
|
||||
}
|
||||
|
||||
const _GB = global.GB;
|
||||
global.GB = (event) => {
|
||||
if (event.t==="weather") draw(event);
|
||||
if (_GB) setTimeout(_GB, 0, event);
|
||||
};
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
const weather = require('weather').load();
|
||||
if (weather) {
|
||||
draw(weather);
|
||||
} else {
|
||||
E.showMessage('Weather unknown\n\nIs Gadgetbridge\nconnected?');
|
||||
}
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
setWatch(Bangle.showLauncher, BTN2, {repeat: false, edge: 'falling'})
|
||||
})()
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AE8N6AXV7vdFyoXBGCQUBAAoXp73u93tC6YWBAAIXSFwQwDRiAWDGASSBmYABIx5IDgYXCmC7KCoYRBnvUCwQwKC4gSEAAgwKC5gwKCwPjC6inBC6owBC6wVKPBXd6YXMDBAuNJJQXWfwZITC/6QIBw073ezR6m73anOJAwuHeBAYFIw4WJAAsL3YQOAAxeBCiWIC4e72AJChGACpMIxAXBIwIwEBIIXKBgouDEIYuLC4ghEC6ELLoYXPU4YXFLxQNBBgcLEQqqQFwYA/AH4AYA=="))
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,176 @@
|
|||
exports = {
|
||||
save: weather => {
|
||||
let json = require('Storage').readJSON('weather.json')||{}
|
||||
json.weather = Object.assign({}, weather) // don't mutate GB events
|
||||
delete json.weather.t // don't save the event type (if present)
|
||||
require('Storage').write('weather.json', json)
|
||||
},
|
||||
load: () => {
|
||||
let json = require('Storage').readJSON('weather.json')||{}
|
||||
return json.weather
|
||||
},
|
||||
drawIcon: (cond, x, y, r) => {
|
||||
function drawSun(x, y, r) {
|
||||
g.setColor("#FF7700");
|
||||
g.fillCircle(x, y, r);
|
||||
}
|
||||
|
||||
function drawCloud(x, y, r, c) {
|
||||
const u = r/12;
|
||||
if (c==null) c = "#EEEEEE";
|
||||
g.setColor(c);
|
||||
g.fillCircle(x-8*u, y+3*u, 4*u);
|
||||
g.fillCircle(x-4*u, y-2*u, 5*u);
|
||||
g.fillCircle(x+4*u, y+0*u, 4*u);
|
||||
g.fillCircle(x+9*u, y+4*u, 3*u);
|
||||
g.fillPoly([
|
||||
x-8*u, y+7*u,
|
||||
x-8*u, y+3*u,
|
||||
x-4*u, y-2*u,
|
||||
x+4*u, y+0*u,
|
||||
x+9*u, y+4*u,
|
||||
x+9*u, y+7*u,
|
||||
]);
|
||||
}
|
||||
|
||||
function drawBrokenClouds(x, y, r) {
|
||||
drawCloud(x+1/8*r, y-1/8*r, 7/8*r, "#777777");
|
||||
drawCloud(x-1/8*r, y+1/8*r, 7/8*r);
|
||||
}
|
||||
|
||||
function drawFewClouds(x, y, r) {
|
||||
drawSun(x+3/8*r, y-1/8*r, 5/8*r);
|
||||
drawCloud(x-1/8*r, y+1/8*r, 7/8*r);
|
||||
}
|
||||
|
||||
function drawRainLines(x, y, r) {
|
||||
g.setColor("#FFFFFF");
|
||||
const y1 = y+1/2*r;
|
||||
const y2 = y+1*r;
|
||||
g.fillPoly([
|
||||
x-6/12*r+1, y1,
|
||||
x-8/12*r+1, y2,
|
||||
x-7/12*r, y2,
|
||||
x-5/12*r, y1,
|
||||
]);
|
||||
g.fillPoly([
|
||||
x-2/12*r+1, y1,
|
||||
x-4/12*r+1, y2,
|
||||
x-3/12*r, y2,
|
||||
x-1/12*r, y1,
|
||||
]);
|
||||
g.fillPoly([
|
||||
x+2/12*r+1, y1,
|
||||
x+0/12*r+1, y2,
|
||||
x+1/12*r, y2,
|
||||
x+3/12*r, y1,
|
||||
]);
|
||||
}
|
||||
|
||||
function drawShowerRain(x, y, r) {
|
||||
drawFewClouds(x, y-1/3*r, r);
|
||||
drawRainLines(x, y, r);
|
||||
}
|
||||
|
||||
function drawRain(x, y, r) {
|
||||
drawBrokenClouds(x, y-1/3*r, r);
|
||||
drawRainLines(x, y, r);
|
||||
}
|
||||
|
||||
function drawThunderstorm(x, y, r) {
|
||||
function drawLightning(x, y, r) {
|
||||
g.setColor("#FF7700");
|
||||
g.fillPoly([
|
||||
x-2/6*r, y-r,
|
||||
x-4/6*r, y+1/6*r,
|
||||
x-1/6*r, y+1/6*r,
|
||||
x-3/6*r, y+1*r,
|
||||
x+3/6*r, y-1/6*r,
|
||||
x+0/6*r, y-1/6*r,
|
||||
x+3/6*r, y-r,
|
||||
]);
|
||||
}
|
||||
|
||||
drawBrokenClouds(x, y-1/3*r, r);
|
||||
drawLightning(x-1/12*r, y+1/2*r, 1/2*r);
|
||||
}
|
||||
|
||||
function drawSnow(x, y, r) {
|
||||
function rotatePoints(points, pivotX, pivotY, angle) {
|
||||
for(let i = 0; i<points.length; i += 2) {
|
||||
const x = points[i];
|
||||
const y = points[i+1];
|
||||
points[i] = Math.cos(angle)*(x-pivotX)-Math.sin(angle)*(y-pivotY)+
|
||||
pivotX;
|
||||
points[i+1] = Math.sin(angle)*(x-pivotX)+Math.cos(angle)*(y-pivotY)+
|
||||
pivotY;
|
||||
}
|
||||
}
|
||||
|
||||
g.setColor("#FFFFFF");
|
||||
const w = 1/12*r;
|
||||
for(let i = 0; i<=6; ++i) {
|
||||
const points = [
|
||||
x+w, y,
|
||||
x-w, y,
|
||||
x-w, y+r,
|
||||
x+w, y+r,
|
||||
];
|
||||
rotatePoints(points, x, y, i/3*Math.PI);
|
||||
g.fillPoly(points);
|
||||
|
||||
for(let j = -1; j<=1; j += 2) {
|
||||
const points = [
|
||||
x+w, y+7/12*r,
|
||||
x-w, y+7/12*r,
|
||||
x-w, y+r,
|
||||
x+w, y+r,
|
||||
];
|
||||
rotatePoints(points, x, y+7/12*r, j/3*Math.PI);
|
||||
rotatePoints(points, x, y, i/3*Math.PI);
|
||||
g.fillPoly(points);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawMist(x, y, r) {
|
||||
const layers = [
|
||||
[-0.4, 0.5],
|
||||
[-0.8, 0.3],
|
||||
[-0.2, 0.9],
|
||||
[-0.9, 0.7],
|
||||
[-0.2, 0.3],
|
||||
];
|
||||
|
||||
g.setColor("#FFFFFF");
|
||||
for(let i = 0; i<5; ++i) {
|
||||
g.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r,
|
||||
y+(0.4*i-0.7)*r-1);
|
||||
g.fillCircle(x+layers[i][0]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5);
|
||||
g.fillCircle(x+layers[i][1]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseIcon(condition) {
|
||||
if (!condition) return () => {};
|
||||
condition = condition.toLowerCase();
|
||||
if (condition.includes("thunderstorm")) return drawThunderstorm;
|
||||
if (condition.includes("freezing")||condition.includes("snow")||
|
||||
condition.includes("sleet")) {
|
||||
return drawSnow;
|
||||
}
|
||||
if (condition.includes("drizzle")||
|
||||
condition.includes("shower")) {
|
||||
return drawRain;
|
||||
}
|
||||
if (condition.includes("rain")) return drawShowerRain;
|
||||
if (condition.includes("clear")) return drawSun;
|
||||
if (condition.includes("few clouds")) return drawFewClouds;
|
||||
if (condition.includes("scattered clouds")) return drawCloud;
|
||||
if (condition.includes("clouds")) return drawBrokenClouds;
|
||||
return drawMist;
|
||||
}
|
||||
|
||||
chooseIcon(cond)(x, y, r)
|
||||
},
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
# Weather
|
||||
|
||||
Shows Gadgetbridge weather reports.
|
||||
|
||||
This adds a widget with a weather pictogram and the temperature.
|
||||
You can view the full report through the app:
|
||||

|
||||
|
||||
## Setup
|
||||
|
||||
See [this guide](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Weather)
|
||||
to setup Gadgetbridge weather reporting.
|
||||
|
||||
## Controls
|
||||
|
||||
BTN2: opens the launcher
|
After Width: | Height: | Size: 3.9 KiB |
|
@ -0,0 +1,40 @@
|
|||
(() => {
|
||||
function draw() {
|
||||
const w = require('weather').load()
|
||||
if (!w) return;
|
||||
g.reset();
|
||||
g.setColor(0).fillRect(this.x, this.y, this.x+this.width, this.y+24)
|
||||
if (w.txt) {
|
||||
require('weather').drawIcon(w.txt, this.x+10, this.y+8, 8);
|
||||
}
|
||||
if (w.temp) {
|
||||
let t = require('locale').temp(w.temp-273.15); // applies conversion
|
||||
t = t.substr(0, t.length-2); // but we have no room for units
|
||||
g.setFontAlign(0, 1); // center horizontally at bottom of widget
|
||||
g.setFont('6x8', 1);
|
||||
g.setColor(-1)
|
||||
g.drawString(t, this.x+10, this.y+24)
|
||||
}
|
||||
}
|
||||
|
||||
function update(weather) {
|
||||
require('weather').save(weather);
|
||||
if (!WIDGETS["weather"].width) {
|
||||
WIDGETS["weather"].width = 20
|
||||
Bangle.drawWidgets()
|
||||
} else if (Bangle.isLCDOn()) {
|
||||
WIDGETS["weather"].draw()
|
||||
}
|
||||
}
|
||||
|
||||
const _GB = global.GB;
|
||||
global.GB = (event) => {
|
||||
if (event.t==="weather") update(event);
|
||||
if (_GB) setTimeout(_GB, 0, event);
|
||||
};
|
||||
|
||||
WIDGETS["weather"] = {area: "tl", width: 20, draw: draw};
|
||||
if (!require('weather').load()) {
|
||||
WIDGETS["weather"].width = 0
|
||||
}
|
||||
})();
|
|
@ -7,3 +7,4 @@
|
|||
0.07: Run again when updated
|
||||
Don't run again when settings app is updated (or absent)
|
||||
Add "Run Now" option to settings
|
||||
0.08: Don't overwrite existing settings on app update
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
(function() {
|
||||
let s = require('Storage').readJSON('welcome.settings.json', 1)
|
||||
let s = require('Storage').readJSON('welcome.json', 1)
|
||||
|| require('Storage').readJSON('setting.json', 1)
|
||||
|| {welcomed: true} // do NOT run if global settings are also absent
|
||||
if (!s.welcomed && require('Storage').read('welcome.app.js')) {
|
||||
setTimeout(() => {
|
||||
s.welcomed = true
|
||||
require('Storage').write('welcome.settings.json', {welcomed: "yes"})
|
||||
require('Storage').write('welcome.json', {welcomed: "yes"})
|
||||
load('welcome.app.js')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"welcomed": false
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
// The welcome app is special, and gets to use global settings
|
||||
(function(back) {
|
||||
let settings = require('Storage').readJSON('welcome.settings.json', 1)
|
||||
let settings = require('Storage').readJSON('welcome.json', 1)
|
||||
|| require('Storage').readJSON('setting.json', 1) || {}
|
||||
E.showMenu({
|
||||
'': { 'title': 'Welcome App' },
|
||||
'Run on Next Boot': {
|
||||
value: !settings.welcomed,
|
||||
format: v => v ? 'OK' : 'No',
|
||||
onchange: v => require('Storage').write('welcome.settings.json', {welcomed: !v}),
|
||||
onchange: v => require('Storage').write('welcome.json', {welcomed: !v}),
|
||||
},
|
||||
'Run Now': () => load('welcome.app.js'),
|
||||
'< Back': back,
|
||||
|
|
|
@ -6,3 +6,5 @@
|
|||
0.07: Add settings: percentage/color/charger icon
|
||||
0.08: Draw percentage as inverted on monochrome battery
|
||||
0.09: Fix regression stopping correct widget updates
|
||||
0.10: Add 'hide if charge greater than'
|
||||
0.11: Don't overwrite existing settings on app update
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* @param {function} back Use back() to return to settings menu
|
||||
*/
|
||||
(function(back) {
|
||||
const SETTINGS_FILE = 'widbatpc.settings.json'
|
||||
const SETTINGS_FILE = 'widbatpc.json'
|
||||
const COLORS = ['By Level', 'Green', 'Monochrome']
|
||||
|
||||
// initialize with default settings...
|
||||
|
@ -11,21 +11,22 @@
|
|||
'color': COLORS[0],
|
||||
'percentage': true,
|
||||
'charger': true,
|
||||
'hideifmorethan': 100,
|
||||
}
|
||||
// ...and overwrite them with any saved values
|
||||
// This way saved values are preserved if a new version adds more settings
|
||||
const storage = require('Storage')
|
||||
const saved = storage.readJSON(SETTINGS_FILE, 1) || {}
|
||||
for (const key in saved) {
|
||||
s[key] = saved[key]
|
||||
s[key] = saved[key];
|
||||
}
|
||||
|
||||
// creates a function to safe a specific setting, e.g. save('color')(1)
|
||||
function save(key) {
|
||||
return function (value) {
|
||||
s[key] = value
|
||||
storage.write(SETTINGS_FILE, s)
|
||||
WIDGETS["batpc"].reload()
|
||||
s[key] = value;
|
||||
storage.write(SETTINGS_FILE, s);
|
||||
WIDGETS["batpc"].reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,8 +52,16 @@
|
|||
const newIndex = (oldIndex + 1) % COLORS.length
|
||||
s.color = COLORS[newIndex]
|
||||
save('color')(s.color)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
'Hide if >': {
|
||||
value: s.hideifmorethan||100,
|
||||
min: 10,
|
||||
max : 100,
|
||||
step: 10,
|
||||
format: x => x+"%",
|
||||
onchange: save('hideifmorethan'),
|
||||
},
|
||||
}
|
||||
E.showMenu(menu)
|
||||
})
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
(function(){
|
||||
const DEFAULTS = {
|
||||
'color': 'By Level',
|
||||
'percentage': true,
|
||||
'charger': true,
|
||||
}
|
||||
const COLORS = {
|
||||
'white': -1,
|
||||
'charging': 0x07E0, // "Green"
|
||||
|
@ -11,15 +6,24 @@ const COLORS = {
|
|||
'ok': 0xFD20, // "Orange"
|
||||
'low':0xF800, // "Red"
|
||||
}
|
||||
const SETTINGS_FILE = 'widbatpc.settings.json'
|
||||
const SETTINGS_FILE = 'widbatpc.json'
|
||||
|
||||
let settings
|
||||
function loadSettings() {
|
||||
settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}
|
||||
const DEFAULTS = {
|
||||
'color': 'By Level',
|
||||
'percentage': true,
|
||||
'charger': true,
|
||||
'hideifmorethan': 100,
|
||||
};
|
||||
Object.keys(DEFAULTS).forEach(k=>{
|
||||
if (settings[k]===undefined) settings[k]=DEFAULTS[k]
|
||||
});
|
||||
}
|
||||
function setting(key) {
|
||||
if (!settings) { loadSettings() }
|
||||
return (key in settings) ? settings[key] : DEFAULTS[key]
|
||||
return settings[key];
|
||||
}
|
||||
|
||||
const levelColor = (l) => {
|
||||
|
@ -45,16 +49,27 @@ const levelColor = (l) => {
|
|||
const chargerColor = () => {
|
||||
return (setting('color') === 'Monochrome') ? COLORS.white : COLORS.charging
|
||||
}
|
||||
|
||||
// sets width, returns true if it changed
|
||||
function setWidth() {
|
||||
WIDGETS["batpc"].width = 40;
|
||||
if (Bangle.isCharging() && setting('charger')) {
|
||||
WIDGETS["batpc"].width += 16;
|
||||
}
|
||||
var w = 40;
|
||||
if (Bangle.isCharging() && setting('charger'))
|
||||
w += 16;
|
||||
if (E.getBattery() > setting('hideifmorethan'))
|
||||
w = 0;
|
||||
var changed = WIDGETS["batpc"].width != w;
|
||||
WIDGETS["batpc"].width = w;
|
||||
return changed;
|
||||
}
|
||||
function draw() {
|
||||
// if hidden, don't draw
|
||||
if (!WIDGETS["batpc"].width) return;
|
||||
// else...
|
||||
var s = 39;
|
||||
var x = this.x, y = this.y;
|
||||
const l = E.getBattery(),
|
||||
c = levelColor(l);
|
||||
const xl = x+4+l*(s-12)/100
|
||||
|
||||
if (Bangle.isCharging() && setting('charger')) {
|
||||
g.setColor(chargerColor()).drawImage(atob(
|
||||
"DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y);
|
||||
|
@ -64,9 +79,7 @@ function draw() {
|
|||
g.fillRect(x,y+2,x+s-4,y+21);
|
||||
g.clearRect(x+2,y+4,x+s-6,y+19);
|
||||
g.fillRect(x+s-3,y+10,x+s,y+14);
|
||||
const l = E.getBattery(),
|
||||
c = levelColor(l);
|
||||
const xl = x+4+l*(s-12)/100
|
||||
|
||||
g.setColor(c).fillRect(x+4,y+6,xl,y+17);
|
||||
g.setColor(-1);
|
||||
if (!setting('percentage')) {
|
||||
|
@ -97,20 +110,24 @@ function reload() {
|
|||
g.clear();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
// update widget - redraw just widget, or all widgets if size changed
|
||||
function update() {
|
||||
if (setWidth()) Bangle.drawWidgets();
|
||||
else WIDGETS["batpc"].draw();
|
||||
}
|
||||
|
||||
Bangle.on('charging',function(charging) {
|
||||
if(charging) Bangle.buzz();
|
||||
setWidth();
|
||||
Bangle.drawWidgets(); // relayout widgets
|
||||
update();
|
||||
g.flip();
|
||||
});
|
||||
var batteryInterval;
|
||||
Bangle.on('lcdPower', function(on) {
|
||||
if (on) {
|
||||
WIDGETS["batpc"].draw();
|
||||
update();
|
||||
// refresh once a minute if LCD on
|
||||
if (!batteryInterval)
|
||||
batteryInterval = setInterval(()=>WIDGETS["batpc"].draw(), 60000);
|
||||
batteryInterval = setInterval(update, 60000);
|
||||
} else {
|
||||
if (batteryInterval) {
|
||||
clearInterval(batteryInterval);
|
||||
|
|
|
@ -113,6 +113,7 @@
|
|||
<div class="panel">
|
||||
<div class="panel-header" style="text-align:right">
|
||||
<button class="btn refresh">Refresh...</button>
|
||||
<button class="btn btn-primary updateapps hidden">Update X apps</button>
|
||||
</div>
|
||||
<div class="panel-body columns"><!-- apps go here --></div>
|
||||
</div>
|
||||
|
|
44
js/index.js
|
@ -207,7 +207,7 @@ function refreshLibrary() {
|
|||
var version = getVersionInfo(app, appInstalled);
|
||||
var versionInfo = version.text;
|
||||
if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>";
|
||||
var readme = `<a href="#" onclick="showReadme('${app.id}')">Read more...</a>`;
|
||||
var readme = `<a class="c-hand" onclick="showReadme('${app.id}')">Read more...</a>`;
|
||||
var favourite = favourites.find(e => e == app.id);
|
||||
return `<div class="tile column col-6 col-sm-12 col-xs-12">
|
||||
<div class="tile-icon">
|
||||
|
@ -218,7 +218,7 @@ function refreshLibrary() {
|
|||
<p class="tile-subtitle">${escapeHtml(app.description)}${app.readme?`<br/>${readme}`:""}</p>
|
||||
<a href="https://github.com/espruino/BangleApps/tree/master/apps/${app.id}" target="_blank" class="link-github"><img src="img/github-icon-sml.png" alt="See the code on GitHub"/></a>
|
||||
</div>
|
||||
<div class="tile-action">
|
||||
<div class="tile-action">
|
||||
<button class="btn btn-link btn-action btn-lg ${!app.custom?"text-error":"d-hide"}" appid="${app.id}" title="Favorite"><i class="icon"></i>${favourite?"♥":"♡"}</button>
|
||||
<button class="btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?"":"d-hide"}" appid="${app.id}" title="Download data from app"><i class="icon icon-download"></i></button>
|
||||
<button class="btn btn-link btn-action btn-lg ${app.allow_emulator?"":"d-hide"}" appid="${app.id}" title="Try in Emulator"><i class="icon icon-share"></i></button>
|
||||
|
@ -401,10 +401,18 @@ function showLoadingIndicator(id) {
|
|||
panelbody.innerHTML = '<div class="tile column col-12"><div class="tile-content" style="min-height:48px;"><div class="loading loading-lg"></div></div></div>';
|
||||
}
|
||||
|
||||
function getAppsToUpdate() {
|
||||
var appsToUpdate = [];
|
||||
appsInstalled.forEach(appInstalled => {
|
||||
var app = appNameToApp(appInstalled.id);
|
||||
if (app.version != appInstalled.version)
|
||||
appsToUpdate.push(app);
|
||||
});
|
||||
return appsToUpdate;
|
||||
}
|
||||
|
||||
function refreshMyApps() {
|
||||
var panelbody = document.querySelector("#myappscontainer .panel-body");
|
||||
var tab = document.querySelector("#tab-myappscontainer a");
|
||||
tab.setAttribute("data-badge", appsInstalled.length);
|
||||
panelbody.innerHTML = appsInstalled.map(appInstalled => {
|
||||
var app = appNameToApp(appInstalled.id);
|
||||
var version = getVersionInfo(app, appInstalled);
|
||||
|
@ -436,6 +444,17 @@ return `<div class="tile column col-6 col-sm-12 col-xs-12">
|
|||
if (icon.classList.contains("icon-download")) handleAppInterface(app);
|
||||
});
|
||||
});
|
||||
var appsToUpdate = getAppsToUpdate();
|
||||
var tab = document.querySelector("#tab-myappscontainer a");
|
||||
var updateApps = document.querySelector("#myappscontainer .updateapps");
|
||||
if (appsToUpdate.length) {
|
||||
updateApps.innerHTML = `Update ${appsToUpdate.length} apps`;
|
||||
updateApps.classList.remove("hidden");
|
||||
tab.setAttribute("data-badge", `${appsInstalled.length} ⬆${appsToUpdate.length}`);
|
||||
} else {
|
||||
updateApps.classList.add("hidden");
|
||||
tab.setAttribute("data-badge", appsInstalled.length);
|
||||
}
|
||||
}
|
||||
|
||||
let haveInstalledApps = false;
|
||||
|
@ -471,6 +490,22 @@ htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addE
|
|||
showToast("Getting app list failed, "+err,"error");
|
||||
});
|
||||
}));
|
||||
htmlToArray(document.querySelectorAll(".btn.updateapps")).map(button => button.addEventListener("click", () => {
|
||||
var appsToUpdate = getAppsToUpdate();
|
||||
var count = appsToUpdate.length;
|
||||
function updater() {
|
||||
if (!appsToUpdate.length) return;
|
||||
var app = appsToUpdate.pop();
|
||||
return updateApp(app).then(function() {
|
||||
return updater();
|
||||
});
|
||||
}
|
||||
updater().then(err => {
|
||||
showToast(`Updated ${count} apps`,"success");
|
||||
}).catch(err => {
|
||||
showToast("Update failed, "+err,"error");
|
||||
});
|
||||
}));
|
||||
connectMyDeviceBtn.addEventListener("click", () => {
|
||||
if (connectMyDeviceBtn.classList.contains('is-connected')) {
|
||||
Comms.disconnectDevice();
|
||||
|
@ -621,4 +656,3 @@ document.getElementById("installfavourite").addEventListener("click",event=>{
|
|||
showToast("App Install failed, "+err,"error");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ function getVersionInfo(appListing, appInstalled) {
|
|||
var versionText = "";
|
||||
var canUpdate = false;
|
||||
function clicky(v) {
|
||||
return `<a href="#" onclick="showChangeLog('${appListing.id}')">${v}</a>`;
|
||||
return `<a class="c-hand" onclick="showChangeLog('${appListing.id}')">${v}</a>`;
|
||||
}
|
||||
|
||||
if (!appInstalled) {
|
||||
|
|