Merge pull request #3419 from bobrippling/feat/run-focus

runplus: add ability to focus an individual stat
pull/3430/head
Rob Pilling 2024-05-21 20:52:22 +01:00 committed by GitHub
commit e39e4b6723
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 103 additions and 47 deletions

View File

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

View File

@ -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).

View File

@ -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);

View File

@ -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"],