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.

463 lines
12 KiB

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