Stian Aune
5 years ago
4 changed files with 446 additions and 52 deletions
-
9background.js
-
459contentScript.js
-
3manifest.json
-
9popup.js
@ -1,38 +1,443 @@ |
|||||
function applyDefaultStyle(element) { |
|
||||
|
if (typeof Overlay === "undefined") { |
||||
|
var Overlay = class { |
||||
|
workout = null; |
||||
|
workoutState = null; |
||||
|
workoutStatus = null; |
||||
|
display = false; |
||||
|
milestones = []; |
||||
|
program = null; |
||||
|
currentCpm = null; |
||||
|
bikes = []; |
||||
|
programs = []; |
||||
|
workouts = []; |
||||
|
bike = null; |
||||
|
setup = null; |
||||
|
|
||||
|
colors = { |
||||
|
"-2": "#F44", |
||||
|
"-1": "#FAA", |
||||
|
"0": "#FFF", |
||||
|
"1": "#AFA", |
||||
|
"2": "#4F4", |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* @param {HTMLElement} mBody |
||||
|
* @param {HTMLElement} mTopLeft |
||||
|
* @param {HTMLElement} mCenter |
||||
|
* @param {HTMLElement} mBottomRight |
||||
|
*/ |
||||
|
constructor(mBody, mTopLeft, mCenter, mBottomRight) { |
||||
|
this.mBody = mBody; |
||||
|
this.mTopLeft = mTopLeft; |
||||
|
this.mCenter = mCenter; |
||||
|
this.mBottomRight = mBottomRight; |
||||
|
|
||||
|
this.initStyle(); |
||||
|
this.hide(); |
||||
|
} |
||||
|
|
||||
|
initStyle() { |
||||
|
this.applyDefaultStyle(this.mTopLeft); |
||||
|
this.applyDefaultStyle(this.mCenter); |
||||
|
this.applyDefaultStyle(this.mBottomRight); |
||||
|
|
||||
|
this.mTopLeft.id = "overlay-mTopLeft"; |
||||
|
this.mTopLeft.style.left = 0; |
||||
|
this.mTopLeft.style.top = "25%"; |
||||
|
|
||||
|
this.mCenter.id = "overlay-mCenter"; |
||||
|
this.mCenter.style.left = "50%"; |
||||
|
this.mCenter.style.top = "50%"; |
||||
|
this.mCenter.style.transform = "translate(-50%, -50%)"; |
||||
|
|
||||
|
this.mBottomRight.id = "overlay-mBottomRight"; |
||||
|
this.mBottomRight.style.right = 0; |
||||
|
this.mBottomRight.style.bottom = 0; |
||||
|
this.mBottomRight.style.fontSize = "2vmax"; |
||||
|
} |
||||
|
|
||||
|
applyDefaultStyle(element) { |
||||
|
this.mBody.append(element); |
||||
|
|
||||
element.style.position = "fixed"; |
element.style.position = "fixed"; |
||||
element.style.zIndex = 99999999; |
element.style.zIndex = 99999999; |
||||
element.style.color = "#FFF"; |
element.style.color = "#FFF"; |
||||
element.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; |
element.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; |
||||
element.style.padding = "0.25em 0.5ch"; |
element.style.padding = "0.25em 0.5ch"; |
||||
element.style.fontSize = "3vmax"; |
element.style.fontSize = "3vmax"; |
||||
|
element.style.lineHeight = "1.33em"; |
||||
|
} |
||||
|
|
||||
|
hide() { |
||||
|
this.mTopLeft.style.display = "none"; |
||||
|
this.mCenter.style.display = "none"; |
||||
|
this.mBottomRight.style.display = "none"; |
||||
|
|
||||
|
this.display = false; |
||||
|
} |
||||
|
|
||||
|
show() { |
||||
|
this.mTopLeft.style.display = "block"; |
||||
|
this.mCenter.style.display = "block"; |
||||
|
this.mBottomRight.style.display = "block"; |
||||
|
|
||||
|
this.display = true; |
||||
|
} |
||||
|
|
||||
|
async setUp() { |
||||
|
this.writeCenter("Loading..."); |
||||
|
|
||||
|
this.bikes = await get("/bike"); |
||||
|
this.programs = await get("/program"); |
||||
|
this.workouts = await get("/workout?active=true"); |
||||
|
|
||||
|
this.writeCenter(null); |
||||
|
this.updateCenterState(); |
||||
|
} |
||||
|
|
||||
|
async updateCenterState() { |
||||
|
const state = this.getState(); |
||||
|
|
||||
|
switch (state) { |
||||
|
case "down": |
||||
|
this.writeCenter(`
|
||||
|
(1) Ny økt<br/> |
||||
|
(2) Fortsett forrige økt |
||||
|
`);
|
||||
|
break; |
||||
|
case "bike": |
||||
|
// TODO: Add this??
|
||||
|
if (this.bikes.length === 1) { |
||||
|
this.setup = {state: "program", bike: this.bikes[0]}; |
||||
|
this.updateCenterState(); |
||||
|
} else { |
||||
|
this.writeCenter("Sykkelvalg ikke støttet"); |
||||
|
} |
||||
|
break; |
||||
|
case "program": |
||||
|
const options = this.programs.map((p, i) => `(${i + 1}) ${p.name}`) |
||||
|
|
||||
|
this.writeCenter("Velg programm:<br/>" + options.join("<br/>")); |
||||
|
break; |
||||
|
case "disconnected": |
||||
|
this.writeCenter("Kobler til..."); |
||||
|
break; |
||||
|
case "connected": |
||||
|
this.writeCenter(`
|
||||
|
(Enter) Start/Pause<br/> |
||||
|
(Esc) Stopp |
||||
|
`);
|
||||
|
break; |
||||
|
case "started": |
||||
|
const calorieDiff = this.calorieDiff(); |
||||
|
if (calorieDiff < 0) { |
||||
|
this.writeCenter(`
|
||||
|
<big style="color: ${this.colors[-1]};"><big><b>${calorieDiff}</b></big> kcal!</big> |
||||
|
`);
|
||||
|
} else { |
||||
|
this.writeCenter(null); |
||||
|
} |
||||
|
break; |
||||
|
default: |
||||
|
this.writeCenter(`Ugyldig tilstand: ${state}`); |
||||
|
} |
||||
|
|
||||
|
this.updateKeyBindings(); |
||||
|
} |
||||
|
|
||||
|
updateKeyBindings() { |
||||
|
const dis = this; |
||||
|
|
||||
|
this.mBody.onkeyup = function (event) { |
||||
|
const state = dis.getState(); |
||||
|
|
||||
|
if (state === "program") { |
||||
|
const i = parseInt(event.key, 10); |
||||
|
if (!isNaN(i) && typeof dis.programs[i - 1] !== "undefined") { |
||||
|
dis.bike = {...dis.setup.bike}; |
||||
|
dis.startWorkout(dis.bike, dis.programs[i - 1]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let handled = false; |
||||
|
switch (event.key) { |
||||
|
case "1": |
||||
|
if (state === "down") { |
||||
|
dis.setup = {state: "bike"}; |
||||
|
handled = true; |
||||
|
} |
||||
|
break; |
||||
|
case "2": |
||||
|
if (state === "down") { |
||||
|
const last = dis.workouts[dis.workouts.length - 1]; |
||||
|
console.log(last); |
||||
|
if (last !== undefined) { |
||||
|
dis.resumeWorkout(last); |
||||
|
} |
||||
|
|
||||
|
handled = true; |
||||
|
} |
||||
|
break; |
||||
|
case "Enter": |
||||
|
if (state === "connected") { |
||||
|
dis.start(); |
||||
|
handled = true; |
||||
|
} else if (state === "started") { |
||||
|
dis.pause(); |
||||
|
handled = true; |
||||
|
} |
||||
|
break; |
||||
|
case "Escape": |
||||
|
if (state === "connected") { |
||||
|
dis.stop(); |
||||
|
handled = true; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (handled) { |
||||
|
dis.updateCenterState(); |
||||
|
event.preventDefault(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async startWorkout(bike, program) { |
||||
|
this.bike = bike; |
||||
|
this.program = program; |
||||
|
|
||||
|
const bikeId = bike.id; |
||||
|
const programId = program.id; |
||||
|
|
||||
|
this.workout = await post("/workout", {bikeId, programId}); |
||||
|
this.updateCenterState(); |
||||
|
|
||||
|
this.workout = await post(`/workout/${this.workout.id}/connect`); |
||||
|
this.updateCenterState(); |
||||
|
|
||||
|
this.setup = null; |
||||
|
this.webSocket(); |
||||
|
} |
||||
|
|
||||
|
async resumeWorkout(workout) { |
||||
|
this.bike = workout.bike; |
||||
|
this.program = workout.program; |
||||
|
this.workout = workout; |
||||
|
|
||||
|
this.setup = null; |
||||
|
this.webSocket(); |
||||
|
} |
||||
|
|
||||
|
async start() { |
||||
|
this.workout = await post(`/workout/${this.workout.id}/start`); |
||||
|
this.updateCenterState(); |
||||
|
} |
||||
|
|
||||
|
async pause() { |
||||
|
this.workout = await post(`/workout/${this.workout.id}/pause`); |
||||
|
this.updateCenterState(); |
||||
|
} |
||||
|
|
||||
|
async stop() { |
||||
|
await post(`/workout/${this.workout.id}/stop`); |
||||
|
|
||||
|
this.workout = null; |
||||
|
this.workouts = await post(`/workout?active=true`); |
||||
|
this.updateCenterState(); |
||||
|
} |
||||
|
|
||||
|
async webSocket() { |
||||
|
let dis = this; |
||||
|
|
||||
|
this.socket = new WebSocket(url(`/workout/${this.workout.id}/subscribe`, "ws")); |
||||
|
|
||||
|
this.socket.onmessage = function ({data, timeStamp}) { |
||||
|
let body = JSON.parse(data); |
||||
|
|
||||
|
if (typeof body.workout !== "undefined") { |
||||
|
dis.workout = {...body.workout, ...dis.workout} |
||||
|
} |
||||
|
|
||||
|
if (typeof body.workoutStatusBackfill !== "undefined") { |
||||
|
body.workoutStatusBackfill.forEach(wsbf => dis.updateWorkoutStatus(wsbf)); |
||||
|
} |
||||
|
|
||||
|
if (typeof body.workoutStatus !== "undefined") { |
||||
|
dis.updateWorkoutStatus(body.workoutStatus) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateWorkoutStatus(newState = null) { |
||||
|
if (newState !== null) { |
||||
|
this.workoutStatus = newState; |
||||
|
} |
||||
|
|
||||
|
if (this.workoutStatus === null) { |
||||
|
this.writeTopLeft(""); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const {minutes, seconds, calories, distance, rpm} = this.workoutStatus; |
||||
|
|
||||
|
const cpmState = this.checkCpm(); |
||||
|
const color = this.colors[cpmState]; |
||||
|
|
||||
|
this.writeTopLeft(`
|
||||
|
<b><big>${pad(minutes)}</big></b> : <b><big>${pad(seconds)}</big></b><br/> |
||||
|
<b style="color: ${color}">${calories}</b> <small>kcal</small><br/> |
||||
|
<b>${distance.toFixed(1)}</b> <small>km</small><br/> |
||||
|
<b>${rpm}</b> <small>rpm</small><br/> |
||||
|
KPM: <b>${this.currentCpm.toFixed(1)}</b><br/> |
||||
|
`);
|
||||
|
|
||||
|
if (seconds === 0 && minutes > 0) { |
||||
|
this.updateMilestones(minutes, calories); |
||||
|
} |
||||
|
|
||||
|
this.updateCenterState(); |
||||
|
} |
||||
|
|
||||
|
checkCpm() { |
||||
|
const {calories, minutes, seconds} = this.workoutStatus; |
||||
|
|
||||
|
this.currentCpm = calories * 60 / ((minutes * 60) + seconds); |
||||
|
if (this.currentCpm === Infinity) { |
||||
|
this.currentCpm = 0; |
||||
|
} |
||||
|
|
||||
|
const program = this.program; |
||||
|
if (program === null || this.currentCpm === 0) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
if (this.calorieDiff() < 0) { |
||||
|
return -1; |
||||
|
} else { |
||||
|
return 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateMilestones(minutes, calories) { |
||||
|
this.milestones.push({minutes, calories}); |
||||
|
|
||||
|
if (minutes % 5 === 0) { |
||||
|
this.milestones = this.milestones.filter(m => m.minutes % 5 === 0) |
||||
|
} |
||||
|
|
||||
|
let lines = ""; |
||||
|
this.milestones.sort((i, j) => j.minutes - i.minutes).forEach(({minutes, calories}) => { |
||||
|
lines += `<tr style="text-align: right"><td>${minutes} </td><td> ${calories}</td></tr>`; |
||||
|
}); |
||||
|
|
||||
|
if (lines.length > 0) { |
||||
|
this.writeBottomRight(`
|
||||
|
<table> |
||||
|
${lines} |
||||
|
</table> |
||||
|
`);
|
||||
|
} else { |
||||
|
this.writeBottomRight(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeTopLeft(message) { |
||||
|
this.mTopLeft.innerHTML = message; |
||||
|
|
||||
|
if (message === null || !this.display) { |
||||
|
this.mTopLeft.style.display = "none"; |
||||
|
} else { |
||||
|
this.mTopLeft.style.display = "block"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeCenter(message) { |
||||
|
this.mCenter.innerHTML = message; |
||||
|
|
||||
|
if (message === null || !this.display) { |
||||
|
this.mCenter.style.display = "none"; |
||||
|
} else { |
||||
|
this.mCenter.style.display = "block"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeBottomRight(message) { |
||||
|
this.mBottomRight.innerHTML = message; |
||||
|
|
||||
|
if (message === null || !this.display) { |
||||
|
this.mBottomRight.style.display = "none"; |
||||
|
} else { |
||||
|
this.mBottomRight.style.display = "block"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
calorieDiff() { |
||||
|
const {minutes, seconds, calories} = this.workoutStatus; |
||||
|
|
||||
|
const expected = this.expectedCalories(minutes, seconds); |
||||
|
return calories - expected; |
||||
|
} |
||||
|
|
||||
|
expectedCalories(minutes, seconds) { |
||||
|
const {warmupMin, warmupCpm, cpm} = this.program; |
||||
|
|
||||
|
let preWarmup = 0; |
||||
|
if (warmupMin > 0) { |
||||
|
// Pre-warmup
|
||||
|
const warmedUpMinutes = Math.min(minutes, warmupMin); |
||||
|
const warmedUpSeconds = warmedUpMinutes >= warmupMin ? 0 : seconds; |
||||
|
preWarmup = Math.round((warmupCpm * (warmedUpMinutes + (warmedUpSeconds / 60)))); |
||||
|
} |
||||
|
|
||||
|
// Post-warmup
|
||||
|
const trainedMinutes = minutes - warmupMin; |
||||
|
const postWarmup = Math.round((cpm * (trainedMinutes + (seconds / 60)))); |
||||
|
|
||||
|
// Sum
|
||||
|
return Math.round(preWarmup + postWarmup); |
||||
|
} |
||||
|
|
||||
|
getState() { |
||||
|
return (this.workout || this.setup || {state: "down"}).state; |
||||
|
} |
||||
|
}; |
||||
} |
} |
||||
|
|
||||
let mCalorieCount = document.createElement("div"); |
|
||||
mCalorieCount.id = "ykonsole-calorie-count"; |
|
||||
applyDefaultStyle(mCalorieCount); |
|
||||
mCalorieCount.style.left = 0; |
|
||||
mCalorieCount.style.top = 0; |
|
||||
mCalorieCount.innerHTML = `
|
|
||||
<b>25</b>:<b>23</b> |
|
||||
<br/> |
|
||||
<b>883</b> kal |
|
||||
`;
|
|
||||
document.body.append(mCalorieCount); |
|
||||
|
|
||||
let mResults = document.createElement("div"); |
|
||||
mResults.id = "ykonsole-result"; |
|
||||
applyDefaultStyle(mResults); |
|
||||
mResults.style.right = 0; |
|
||||
mResults.style.bottom = 0; |
|
||||
mResults.style.textAlign = "right"; |
|
||||
mResults.innerHTML = `
|
|
||||
5 161<br/> |
|
||||
10 332<br/> |
|
||||
15 490<br/> |
|
||||
20 712<br/> |
|
||||
25 875<br/> |
|
||||
`;
|
|
||||
document.body.append(mResults); |
|
||||
|
function pad(number) { |
||||
|
return number >= 10 ? `${number}` : `0${number}`; |
||||
|
} |
||||
|
|
||||
|
function url(path, prefix = "http") { |
||||
|
return `${prefix}://127.0.0.1:9999/api${path}`; |
||||
|
} |
||||
|
|
||||
|
function get(path) { |
||||
|
return fetch(url(path), { |
||||
|
method: "GET", |
||||
|
}).then(r => r.json()); |
||||
|
} |
||||
|
|
||||
|
function post(path, data = {}) { |
||||
|
return fetch(url(path), { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
}, |
||||
|
body: JSON.stringify(data) |
||||
|
}).then(r => r.json()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const overlay = |
||||
|
new Overlay( |
||||
|
document.body, |
||||
|
document.createElement("div"), |
||||
|
document.createElement("div"), |
||||
|
document.createElement("div")); |
||||
|
|
||||
|
overlay.show(); |
||||
|
overlay.setUp(); |
||||
|
|
||||
chrome.storage.sync.get('color', ({color}) => { |
chrome.storage.sync.get('color', ({color}) => { |
||||
}); |
}); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue