You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

448 lines
11 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. if (typeof Overlay === "undefined") {
  2. var Overlay = class {
  3. workout = null;
  4. workoutState = null;
  5. workoutStatus = null;
  6. display = false;
  7. milestones = [];
  8. program = null;
  9. currentCpm = null;
  10. bikes = [];
  11. programs = [];
  12. workouts = [];
  13. bike = null;
  14. setup = null;
  15. colors = {
  16. "-2": "#F44",
  17. "-1": "#FAA",
  18. "0": "#FFF",
  19. "1": "#AFA",
  20. "2": "#4F4",
  21. };
  22. /**
  23. * @param {HTMLElement} mBody
  24. * @param {HTMLElement} mTopLeft
  25. * @param {HTMLElement} mCenter
  26. * @param {HTMLElement} mBottomRight
  27. */
  28. constructor(mBody, mTopLeft, mCenter, mBottomRight) {
  29. this.mBody = mBody;
  30. this.mTopLeft = mTopLeft;
  31. this.mCenter = mCenter;
  32. this.mBottomRight = mBottomRight;
  33. this.initStyle();
  34. this.hide();
  35. }
  36. initStyle() {
  37. this.applyDefaultStyle(this.mTopLeft);
  38. this.applyDefaultStyle(this.mCenter);
  39. this.applyDefaultStyle(this.mBottomRight);
  40. this.mTopLeft.id = "overlay-mTopLeft";
  41. this.mTopLeft.style.left = 0;
  42. this.mTopLeft.style.top = "25%";
  43. this.mCenter.id = "overlay-mCenter";
  44. this.mCenter.style.left = "50%";
  45. this.mCenter.style.top = "50%";
  46. this.mCenter.style.transform = "translate(-50%, -50%)";
  47. this.mBottomRight.id = "overlay-mBottomRight";
  48. this.mBottomRight.style.right = 0;
  49. this.mBottomRight.style.bottom = 0;
  50. this.mBottomRight.style.fontSize = "2vmax";
  51. }
  52. applyDefaultStyle(element) {
  53. this.mBody.append(element);
  54. element.style.position = "fixed";
  55. element.style.zIndex = 99999999;
  56. element.style.color = "#FFF";
  57. element.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
  58. element.style.padding = "0.25em 0.5ch";
  59. element.style.fontSize = "3vmax";
  60. element.style.lineHeight = "1.33em";
  61. }
  62. hide() {
  63. this.mTopLeft.style.display = "none";
  64. this.mCenter.style.display = "none";
  65. this.mBottomRight.style.display = "none";
  66. this.display = false;
  67. }
  68. show() {
  69. this.mTopLeft.style.display = "block";
  70. this.mCenter.style.display = "block";
  71. this.mBottomRight.style.display = "block";
  72. this.display = true;
  73. }
  74. async setUp() {
  75. this.writeCenter("Loading...");
  76. this.bikes = await get("/bike");
  77. this.programs = await get("/program");
  78. this.workouts = await get("/workout?active=true");
  79. this.writeCenter(null);
  80. this.updateCenterState();
  81. }
  82. async updateCenterState() {
  83. const state = this.getState();
  84. switch (state) {
  85. case "down":
  86. this.writeCenter(`
  87. (1) Ny økt<br/>
  88. (2) Fortsett forrige økt
  89. `);
  90. break;
  91. case "bike":
  92. // TODO: Add this??
  93. if (this.bikes.length === 1) {
  94. this.setup = {state: "program", bike: this.bikes[0]};
  95. this.updateCenterState();
  96. } else {
  97. this.writeCenter("Sykkelvalg ikke støttet");
  98. }
  99. break;
  100. case "program":
  101. const options = this.programs.map((p, i) => `(${i + 1}) ${p.name}`)
  102. this.writeCenter("Velg programm:<br/>" + options.join("<br/>"));
  103. break;
  104. case "disconnected":
  105. this.writeCenter("Kobler til...");
  106. break;
  107. case "connected":
  108. this.writeCenter(`
  109. (Enter) Start/Pause<br/>
  110. (Esc) Stopp
  111. `);
  112. break;
  113. case "started":
  114. const calorieDiff = this.calorieDiff();
  115. if (calorieDiff < 0) {
  116. this.writeCenter(`
  117. <big style="color: ${this.colors[-1]};"><big><b>${calorieDiff}</b></big> kcal!</big>
  118. `);
  119. } else {
  120. this.writeCenter(null);
  121. }
  122. break;
  123. default:
  124. this.writeCenter(`Ugyldig tilstand: ${state}`);
  125. }
  126. this.updateKeyBindings();
  127. }
  128. updateKeyBindings() {
  129. const dis = this;
  130. this.mBody.onkeyup = function (event) {
  131. const state = dis.getState();
  132. if (state === "program") {
  133. const i = parseInt(event.key, 10);
  134. if (!isNaN(i) && typeof dis.programs[i - 1] !== "undefined") {
  135. dis.bike = {...dis.setup.bike};
  136. dis.startWorkout(dis.bike, dis.programs[i - 1]);
  137. }
  138. }
  139. let handled = false;
  140. switch (event.key) {
  141. case "1":
  142. if (state === "down") {
  143. dis.setup = {state: "bike"};
  144. handled = true;
  145. }
  146. break;
  147. case "2":
  148. if (state === "down") {
  149. const last = dis.workouts[dis.workouts.length - 1];
  150. console.log(last);
  151. if (last !== undefined) {
  152. dis.resumeWorkout(last);
  153. }
  154. handled = true;
  155. }
  156. break;
  157. case "Enter":
  158. if (state === "connected") {
  159. dis.start();
  160. handled = true;
  161. } else if (state === "started") {
  162. dis.pause();
  163. handled = true;
  164. }
  165. break;
  166. case "Escape":
  167. if (state === "connected") {
  168. dis.stop();
  169. handled = true;
  170. }
  171. break;
  172. }
  173. if (handled) {
  174. dis.updateCenterState();
  175. event.preventDefault();
  176. }
  177. }
  178. }
  179. async startWorkout(bike, program) {
  180. this.bike = bike;
  181. this.program = program;
  182. const bikeId = bike.id;
  183. const programId = program.id;
  184. this.workout = await post("/workout", {bikeId, programId});
  185. this.updateCenterState();
  186. this.workout = await post(`/workout/${this.workout.id}/connect`);
  187. this.updateCenterState();
  188. this.setup = null;
  189. this.webSocket();
  190. }
  191. async resumeWorkout(workout) {
  192. this.bike = workout.bike;
  193. this.program = workout.program;
  194. this.workout = workout;
  195. this.setup = null;
  196. this.webSocket();
  197. }
  198. async start() {
  199. this.workout = await post(`/workout/${this.workout.id}/start`);
  200. this.updateCenterState();
  201. }
  202. async pause() {
  203. this.workout = await post(`/workout/${this.workout.id}/pause`);
  204. this.updateCenterState();
  205. }
  206. async stop() {
  207. await post(`/workout/${this.workout.id}/stop`);
  208. this.workout = null;
  209. this.workouts = await post(`/workout?active=true`);
  210. this.updateCenterState();
  211. }
  212. async webSocket() {
  213. let dis = this;
  214. this.socket = new WebSocket(url(`/workout/${this.workout.id}/subscribe`, "ws"));
  215. this.socket.onmessage = function ({data, timeStamp}) {
  216. let body = JSON.parse(data);
  217. if (typeof body.workout !== "undefined") {
  218. dis.workout = {...body.workout, ...dis.workout}
  219. }
  220. if (typeof body.workoutStatusBackfill !== "undefined") {
  221. body.workoutStatusBackfill.forEach(wsbf => dis.updateWorkoutStatus(wsbf));
  222. }
  223. if (typeof body.workoutStatus !== "undefined") {
  224. dis.updateWorkoutStatus(body.workoutStatus)
  225. }
  226. }
  227. }
  228. updateWorkoutStatus(newState = null) {
  229. if (newState !== null) {
  230. this.workoutStatus = newState;
  231. }
  232. if (this.workoutStatus === null) {
  233. this.writeTopLeft("");
  234. return;
  235. }
  236. const {minutes, seconds, calories, distance, rpm} = this.workoutStatus;
  237. const cpmState = this.checkCpm();
  238. const color = this.colors[cpmState];
  239. this.writeTopLeft(`
  240. <b><big>${pad(minutes)}</big></b> : <b><big>${pad(seconds)}</big></b><br/>
  241. <b style="color: ${color}">${calories}</b> <small>kcal</small><br/>
  242. <b>${distance.toFixed(1)}</b> <small>km</small><br/>
  243. <b>${rpm}</b> <small>rpm</small><br/>
  244. KPM: <b>${this.currentCpm.toFixed(1)}</b><br/>
  245. `);
  246. if (seconds === 0 && minutes > 0) {
  247. this.updateMilestones(minutes, calories);
  248. }
  249. this.updateCenterState();
  250. }
  251. checkCpm() {
  252. const {calories, minutes, seconds} = this.workoutStatus;
  253. this.currentCpm = calories * 60 / ((minutes * 60) + seconds);
  254. if (this.currentCpm === Infinity) {
  255. this.currentCpm = 0;
  256. }
  257. const program = this.program;
  258. if (program === null || this.currentCpm === 0) {
  259. return 0;
  260. }
  261. if (this.calorieDiff() < 0) {
  262. return -1;
  263. } else {
  264. return 1;
  265. }
  266. }
  267. updateMilestones(minutes, calories) {
  268. this.milestones.push({minutes, calories});
  269. if (minutes % 5 === 0) {
  270. this.milestones = this.milestones.filter(m => m.minutes % 5 === 0)
  271. }
  272. let lines = "";
  273. this.milestones.sort((i, j) => j.minutes - i.minutes).forEach(({minutes, calories}) => {
  274. lines += `<tr style="text-align: right"><td>${minutes}&nbsp;</td><td>&nbsp;${calories}</td></tr>`;
  275. });
  276. if (lines.length > 0) {
  277. this.writeBottomRight(`
  278. <table>
  279. ${lines}
  280. </table>
  281. `);
  282. } else {
  283. this.writeBottomRight(null);
  284. }
  285. }
  286. writeTopLeft(message) {
  287. this.mTopLeft.innerHTML = message;
  288. if (message === null || !this.display) {
  289. this.mTopLeft.style.display = "none";
  290. } else {
  291. this.mTopLeft.style.display = "block";
  292. }
  293. }
  294. writeCenter(message) {
  295. this.mCenter.innerHTML = message;
  296. if (message === null || !this.display) {
  297. this.mCenter.style.display = "none";
  298. } else {
  299. this.mCenter.style.display = "block";
  300. }
  301. }
  302. writeBottomRight(message) {
  303. this.mBottomRight.innerHTML = message;
  304. if (message === null || !this.display) {
  305. this.mBottomRight.style.display = "none";
  306. } else {
  307. this.mBottomRight.style.display = "block";
  308. }
  309. }
  310. calorieDiff() {
  311. if (this.workoutStatus == null) {
  312. return 0;
  313. }
  314. const {minutes, seconds, calories} = this.workoutStatus;
  315. const expected = this.expectedCalories(minutes, seconds);
  316. return calories - expected;
  317. }
  318. expectedCalories(minutes, seconds) {
  319. const {warmupMin, warmupCpm, cpm} = this.program;
  320. let preWarmup = 0;
  321. if (warmupMin > 0) {
  322. // Pre-warmup
  323. const warmedUpMinutes = Math.min(minutes, warmupMin);
  324. const warmedUpSeconds = minutes >= warmupMin ? 0 : seconds;
  325. preWarmup = Math.round((warmupCpm * (warmedUpMinutes + (warmedUpSeconds / 60))));
  326. }
  327. // Post-warmup
  328. const trainedMinutes = Math.min(0, minutes - warmupMin);
  329. const trainedSeconds = minutes >= warmupMin ? seconds : 0;
  330. const postWarmup = Math.round((cpm * (trainedMinutes + (trainedSeconds / 60))));
  331. // Sum
  332. return Math.round(preWarmup + postWarmup);
  333. }
  334. getState() {
  335. return (this.workout || this.setup || {state: "down"}).state;
  336. }
  337. };
  338. }
  339. function pad(number) {
  340. return number >= 10 ? `${number}` : `0${number}`;
  341. }
  342. function url(path, prefix = "http") {
  343. return `${prefix}://127.0.0.1:9999/api${path}`;
  344. }
  345. function get(path) {
  346. return fetch(url(path), {
  347. method: "GET",
  348. }).then(r => r.json());
  349. }
  350. function post(path, data = {}) {
  351. return fetch(url(path), {
  352. method: "POST",
  353. headers: {
  354. "Content-Type": "application/json",
  355. },
  356. body: JSON.stringify(data)
  357. }).then(r => r.json());
  358. }
  359. const overlay =
  360. new Overlay(
  361. document.body,
  362. document.createElement("div"),
  363. document.createElement("div"),
  364. document.createElement("div"));
  365. overlay.show();
  366. overlay.setUp();
  367. chrome.storage.sync.get('color', ({color}) => {
  368. });