mirror of https://github.com/espruino/BangleApps
Merge pull request #3419 from bobrippling/feat/run-focus
runplus: add ability to focus an individual statpull/3430/head
commit
e39e4b6723
|
@ -26,3 +26,4 @@ Write to correct settings file, fixing settings not working.
|
||||||
0.23: Minor code improvements
|
0.23: Minor code improvements
|
||||||
0.24: Add indicators for lock,gps and pulse to karvonen screen
|
0.24: Add indicators for lock,gps and pulse to karvonen screen
|
||||||
0.25: Fix step count bug when runs are resumed after a long time
|
0.25: Fix step count bug when runs are resumed after a long time
|
||||||
|
0.26: Add ability to zoom in on a single stat by tapping it
|
||||||
|
|
|
@ -4,6 +4,8 @@ Displays distance, time, steps, cadence, pace and heart rate for runners. Based
|
||||||
It requires the input of your minimum and maximum heart rate in the settings for the app to work. You can come back back to the initial run screen anytime by swimping left.
|
It requires the input of your minimum and maximum heart rate in the settings for the app to work. You can come back back to the initial run screen anytime by swimping left.
|
||||||
To use it, start the app and press the middle button so that the red STOP in the bottom right turns to a green `RUN`.
|
To use it, start the app and press the middle button so that the red STOP in the bottom right turns to a green `RUN`.
|
||||||
|
|
||||||
|
To focus on a single stat, tap on the stat and it will take up the full screen. Tap again to return to the main screen.
|
||||||
|
|
||||||
## Display 1st screen
|
## Display 1st screen
|
||||||
|
|
||||||
* `DIST` - the distance travelled based on the GPS (if you have a GPS lock).
|
* `DIST` - the distance travelled based on the GPS (if you have a GPS lock).
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
let runInterval;
|
let runInterval;
|
||||||
let karvonenActive = false;
|
let screen = "main"; // main | karvonen | menu | zoom
|
||||||
// Run interface wrapped in a function
|
// Run interface wrapped in a function
|
||||||
const ExStats = require("exstats");
|
const ExStats = require("exstats");
|
||||||
let B2 = process.env.HWVERSION===2;
|
let B2 = process.env.HWVERSION===2;
|
||||||
|
@ -7,9 +7,10 @@ let Layout = require("Layout");
|
||||||
let locale = require("locale");
|
let locale = require("locale");
|
||||||
let fontHeading = "6x8:2";
|
let fontHeading = "6x8:2";
|
||||||
let fontValue = B2 ? "6x15:2" : "6x8:3";
|
let fontValue = B2 ? "6x15:2" : "6x8:3";
|
||||||
|
let zoomFont = "12x20:3";
|
||||||
|
let zoomFontSmall = "12x20:2";
|
||||||
let headingCol = "#888";
|
let headingCol = "#888";
|
||||||
let fixCount = 0;
|
let fixCount = 0;
|
||||||
let isMenuDisplayed = false;
|
|
||||||
const wu = require("widget_utils");
|
const wu = require("widget_utils");
|
||||||
|
|
||||||
g.reset().clear();
|
g.reset().clear();
|
||||||
|
@ -52,11 +53,16 @@ function setStatus(running) {
|
||||||
layout.button.label = running ? "STOP" : "START";
|
layout.button.label = running ? "STOP" : "START";
|
||||||
layout.status.label = running ? "RUN" : "STOP";
|
layout.status.label = running ? "RUN" : "STOP";
|
||||||
layout.status.bgCol = running ? "#0f0" : "#f00";
|
layout.status.bgCol = running ? "#0f0" : "#f00";
|
||||||
layout.render();
|
if (screen === "main") layout.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called to start/stop running
|
// Called to start/stop running
|
||||||
function onStartStop() {
|
function onStartStop() {
|
||||||
|
if (screen === "karvonen") {
|
||||||
|
// start/stop on the karvonen screen reverts us to the main screen
|
||||||
|
setScreen("main");
|
||||||
|
}
|
||||||
|
|
||||||
var running = !exs.state.active;
|
var running = !exs.state.active;
|
||||||
var shouldResume = false;
|
var shouldResume = false;
|
||||||
var promise = Promise.resolve();
|
var promise = Promise.resolve();
|
||||||
|
@ -64,10 +70,10 @@ function onStartStop() {
|
||||||
if (running && exs.state.duration > 10000) { // if more than 10 seconds of duration, ask if we should resume?
|
if (running && exs.state.duration > 10000) { // if more than 10 seconds of duration, ask if we should resume?
|
||||||
promise = promise.
|
promise = promise.
|
||||||
then(() => {
|
then(() => {
|
||||||
isMenuDisplayed = true;
|
screen = "menu";
|
||||||
return E.showPrompt("Resume run?",{title:"Run"});
|
return E.showPrompt("Resume run?",{title:"Run"});
|
||||||
}).then(r => {
|
}).then(r => {
|
||||||
isMenuDisplayed=false;
|
screen = "main";
|
||||||
layout.setUI(); // grab our input handling again
|
layout.setUI(); // grab our input handling again
|
||||||
layout.forgetLazyState();
|
layout.forgetLazyState();
|
||||||
layout.render();
|
layout.render();
|
||||||
|
@ -80,11 +86,11 @@ function onStartStop() {
|
||||||
// an overwrite before we start tracking exstats
|
// an overwrite before we start tracking exstats
|
||||||
if (settings.record && WIDGETS["recorder"]) {
|
if (settings.record && WIDGETS["recorder"]) {
|
||||||
if (running) {
|
if (running) {
|
||||||
isMenuDisplayed = true;
|
screen = "menu";
|
||||||
promise = promise.
|
promise = promise.
|
||||||
then(() => WIDGETS["recorder"].setRecording(true, { force : shouldResume?"append":undefined })).
|
then(() => WIDGETS["recorder"].setRecording(true, { force : shouldResume?"append":undefined })).
|
||||||
then(() => {
|
then(() => {
|
||||||
isMenuDisplayed = false;
|
screen = "main";
|
||||||
layout.setUI(); // grab our input handling again
|
layout.setUI(); // grab our input handling again
|
||||||
layout.forgetLazyState();
|
layout.forgetLazyState();
|
||||||
layout.render();
|
layout.render();
|
||||||
|
@ -99,7 +105,7 @@ function onStartStop() {
|
||||||
promise.then(() => {
|
promise.then(() => {
|
||||||
if (running) {
|
if (running) {
|
||||||
if (shouldResume)
|
if (shouldResume)
|
||||||
exs.resume()
|
exs.resume();
|
||||||
else
|
else
|
||||||
exs.start();
|
exs.start();
|
||||||
} else {
|
} else {
|
||||||
|
@ -111,23 +117,68 @@ function onStartStop() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function zoom(statID) {
|
||||||
|
if (screen !== "main") return;
|
||||||
|
|
||||||
|
setScreen("zoom");
|
||||||
|
|
||||||
|
const onTouch = () => {
|
||||||
|
Bangle.removeListener("touch", onTouch);
|
||||||
|
Bangle.removeListener("twist", onTwist);
|
||||||
|
stat.removeListener("changed", draw);
|
||||||
|
setScreen("main");
|
||||||
|
};
|
||||||
|
Bangle.on("touch", onTouch); // queued after layout's touchHandler (otherwise we'd be removed then instantly re-zoomed)
|
||||||
|
|
||||||
|
const onTwist = () => {
|
||||||
|
Bangle.setLCDPower(1);
|
||||||
|
};
|
||||||
|
Bangle.on("twist", onTwist);
|
||||||
|
|
||||||
|
const draw = stat => {
|
||||||
|
const R = Bangle.appRect;
|
||||||
|
|
||||||
|
g.reset()
|
||||||
|
.clearRect(R)
|
||||||
|
.setFontAlign(0, 0);
|
||||||
|
|
||||||
|
layout.render(layout.bottom);
|
||||||
|
|
||||||
|
const value = exs.state.active ? stat.getString() : "____";
|
||||||
|
|
||||||
|
g
|
||||||
|
.setFont(stat.title.length > 5 ? zoomFontSmall : zoomFont)
|
||||||
|
.setColor(headingCol)
|
||||||
|
.drawString(stat.title.toUpperCase(), R.x+R.w/2, R.y+R.h/3)
|
||||||
|
.setColor(g.theme.fg)
|
||||||
|
.drawString(value, R.x+R.w/2, R.y+R.h*2/3);
|
||||||
|
};
|
||||||
|
layout.lazy = false; // restored when we go back to "main"
|
||||||
|
|
||||||
|
const stat = exs.stats[statID];
|
||||||
|
stat.on("changed", draw);
|
||||||
|
draw(stat);
|
||||||
|
}
|
||||||
|
|
||||||
let lc = [];
|
let lc = [];
|
||||||
// Load stats in pair by pair
|
// Load stats in pair by pair
|
||||||
for (let i=0;i<statIDs.length;i+=2) {
|
for (let i=0;i<statIDs.length;i+=2) {
|
||||||
let sa = exs.stats[statIDs[i+0]];
|
let sa = exs.stats[statIDs[i+0]];
|
||||||
let sb = exs.stats[statIDs[i+1]];
|
let sb = exs.stats[statIDs[i+1]];
|
||||||
|
let cba = zoom.bind(null, statIDs[i]);
|
||||||
|
let cbb = zoom.bind(null, statIDs[i+1]);
|
||||||
lc.push({ type:"h", filly:1, c:[
|
lc.push({ type:"h", filly:1, c:[
|
||||||
sa?{type:"txt", font:fontHeading, label:sa.title.toUpperCase(), fillx:1, col:headingCol }:{},
|
sa?{type:"txt", font:fontHeading, label:sa.title.toUpperCase(), fillx:1, col:headingCol, cb: cba }:{},
|
||||||
sb?{type:"txt", font:fontHeading, label:sb.title.toUpperCase(), fillx:1, col:headingCol }:{}
|
sb?{type:"txt", font:fontHeading, label:sb.title.toUpperCase(), fillx:1, col:headingCol, cb: cbb }:{}
|
||||||
]}, { type:"h", filly:1, c:[
|
]}, { type:"h", filly:1, c:[
|
||||||
sa?{type:"txt", font:fontValue, label:sa.getString(), id:sa.id, fillx:1 }:{},
|
sa?{type:"txt", font:fontValue, label:sa.getString(), id:sa.id, fillx:1, cb: cba }:{},
|
||||||
sb?{type:"txt", font:fontValue, label:sb.getString(), id:sb.id, fillx:1 }:{}
|
sb?{type:"txt", font:fontValue, label:sb.getString(), id:sb.id, fillx:1, cb: cbb }:{}
|
||||||
]});
|
]});
|
||||||
if (sa) sa.on('changed', e=>layout[e.id].label = e.getString());
|
if (sa) sa.on('changed', e=>layout[e.id].label = e.getString());
|
||||||
if (sb) sb.on('changed', e=>layout[e.id].label = e.getString());
|
if (sb) sb.on('changed', e=>layout[e.id].label = e.getString());
|
||||||
}
|
}
|
||||||
// At the bottom put time/GPS state/etc
|
// At the bottom put time/GPS state/etc
|
||||||
lc.push({ type:"h", filly:1, c:[
|
lc.push({ type:"h", id:"bottom", filly:1, c:[
|
||||||
{type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" },
|
{type:"txt", font:fontHeading, label:"GPS", id:"gps", fillx:1, bgCol:"#f00" },
|
||||||
{type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
|
{type:"txt", font:fontHeading, label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg },
|
||||||
{type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 }
|
{type:"txt", font:fontHeading, label:"---", id:"status", fillx:1 }
|
||||||
|
@ -135,7 +186,7 @@ lc.push({ type:"h", filly:1, c:[
|
||||||
// Now calculate the layout
|
// Now calculate the layout
|
||||||
let layout = new Layout( {
|
let layout = new Layout( {
|
||||||
type:"v", c: lc
|
type:"v", c: lc
|
||||||
},{lazy:true, btns:[{ label:"---", cb: (()=>{if (karvonenActive) {run();} onStartStop();}), id:"button"}]});
|
},{lazy:true, btns:[{ label:"---", cb: onStartStop, id:"button"}]});
|
||||||
delete lc;
|
delete lc;
|
||||||
setStatus(exs.state.active);
|
setStatus(exs.state.active);
|
||||||
layout.render();
|
layout.render();
|
||||||
|
@ -165,47 +216,49 @@ Bangle.on("GPS", function(fix) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// run() function used to start updating traditional run ui
|
function setScreen(to) {
|
||||||
function run() {
|
if (screen === "karvonen") {
|
||||||
require("runplus_karvonen").stop();
|
require("runplus_karvonen").stop();
|
||||||
karvonenActive = false;
|
wu.show();
|
||||||
wu.show();
|
Bangle.drawWidgets();
|
||||||
Bangle.drawWidgets();
|
|
||||||
g.reset().clearRect(Bangle.appRect);
|
|
||||||
layout.lazy = false;
|
|
||||||
layout.render();
|
|
||||||
layout.lazy = true;
|
|
||||||
// We always call ourselves once a second to update
|
|
||||||
if (!runInterval){
|
|
||||||
runInterval = setInterval(function() {
|
|
||||||
layout.clock.label = locale.time(new Date(),1);
|
|
||||||
if (!isMenuDisplayed) layout.render();
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
run();
|
|
||||||
|
|
||||||
///////////////////////////////////////////////
|
|
||||||
// Karvonen
|
|
||||||
///////////////////////////////////////////////
|
|
||||||
|
|
||||||
function karvonen(){
|
|
||||||
// stop updating and drawing the traditional run app UI
|
|
||||||
if (runInterval) clearInterval(runInterval);
|
if (runInterval) clearInterval(runInterval);
|
||||||
runInterval = undefined;
|
runInterval = undefined;
|
||||||
g.reset().clearRect(Bangle.appRect);
|
g.reset().clearRect(Bangle.appRect);
|
||||||
require("runplus_karvonen").start(settings.HRM, exs.stats.bpm);
|
|
||||||
karvonenActive = true;
|
screen = to;
|
||||||
|
switch (screen) {
|
||||||
|
case "main":
|
||||||
|
layout.lazy = false;
|
||||||
|
layout.render();
|
||||||
|
layout.lazy = true;
|
||||||
|
// We always call ourselves once a second to update
|
||||||
|
if (!runInterval){
|
||||||
|
runInterval = setInterval(function() {
|
||||||
|
layout.clock.label = locale.time(new Date(),1);
|
||||||
|
if (screen !== "menu") layout.render();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "karvonen":
|
||||||
|
require("runplus_karvonen").start(settings.HRM, exs.stats.bpm);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the function to go back and forth between the different UI's
|
// Define the function to go back and forth between the different UI's
|
||||||
function swipeHandler(LR,_) {
|
function swipeHandler(LR,_) {
|
||||||
if (!isMenuDisplayed){
|
if (screen !== "menu"){
|
||||||
if (LR==-1 && karvonenActive)
|
if (LR < 0 && screen == "karvonen")
|
||||||
run();
|
setScreen("main");
|
||||||
if (LR==1 && !karvonenActive)
|
if (LR > 0 && screen !== "karvonen")
|
||||||
karvonen();
|
setScreen("karvonen"); // stop updating and drawing the traditional run app UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setScreen("main");
|
||||||
|
|
||||||
// Listen for swipes with the swipeHandler
|
// Listen for swipes with the swipeHandler
|
||||||
Bangle.on("swipe", swipeHandler);
|
Bangle.on("swipe", swipeHandler);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"id": "runplus",
|
"id": "runplus",
|
||||||
"name": "Run+",
|
"name": "Run+",
|
||||||
"version": "0.25",
|
"version": "0.26",
|
||||||
"description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.",
|
"description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screens for heart rate interval training and individual stat focus.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen",
|
"tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen",
|
||||||
"supports": ["BANGLEJS2"],
|
"supports": ["BANGLEJS2"],
|
||||||
|
|
Loading…
Reference in New Issue