diff --git a/controllers.js b/controllers.js index edff44f..74e70b4 100644 --- a/controllers.js +++ b/controllers.js @@ -9,18 +9,17 @@ function DeviceController(args) { let hrb = args.hrb; let watch = args.watch; - xf.sub('db:targetPwr', e => { - let targetPwr = e.detail.data.targetPwr; + xf.sub('db:targetPwr', targetPwr => { + // let targetPwr = e.detail.data.targetPwr; controllable.setTargetPower(targetPwr); }); - xf.sub('db:resistanceTarget', e => { - let resistance = e.detail.data.resistanceTarget; - // resistance *= 10; + xf.sub('db:resistanceTarget', resistanceTarget => { + let resistance = resistanceTarget; resistance = parseInt(resistance); controllable.setTargetResistanceLevel(resistance); }); - xf.sub('db:slopeTarget', e => { - let slope = e.detail.data.slopeTarget; + xf.sub('db:slopeTarget', slopeTarget => { + let slope = slopeTarget; slope *= 100; slope = parseInt(slope); controllable.setSimulationParameters({grade: slope}); @@ -29,7 +28,28 @@ function DeviceController(args) { xf.sub('ui:watchPause', e => { watch.pause(); }); xf.sub('ui:watchResume', e => { watch.resume(); }); xf.sub('ui:watchLap', e => { watch.lap(); }); - xf.sub('ui:watchStop', e => { watch.stop(); }); + xf.sub('ui:watchStop', e => { + const stop = confirm('Confirm Stop?'); + if(stop) { + watch.stop(); + } + }); + + + xf.sub(`session:watchRestore`, session => { + watch.elapsed = session.elapsed; + watch.lapTime = session.lapTime; + watch.stepTime = session.stepTime; + + if(session.watchState === 'started') { + watch.workoutStarted = true; + xf.dispatch('ui:watchStart'); + } + if(session.watchState === 'paused') { + watch.workoutStarted = true; + } + + }); xf.sub('ui:controllableSwitch', e => { if(controllable.device.connected) { @@ -81,8 +101,8 @@ function Screen() { function FileController() { - xf.sub('db:workoutFile', e => { - let workoutFile = e.detail.data.workoutFile; + xf.sub('db:workoutFile', workoutFile => { + // let workoutFile = e.detail.data.workoutFile; let fileHandler = new FileHandler(); fileHandler.readFile(workoutFile); }); diff --git a/css/flux.css b/css/flux.css index faf2e3c..ca04a92 100644 --- a/css/flux.css +++ b/css/flux.css @@ -211,7 +211,11 @@ body.white-theme { } .white-theme .devices { background-color: var(--white); - box-shadow: 0px 1px 1px 0px var(--black-opacity-01); + + /* color: var(--white); */ + /* background-color: var(--chrome); */ + + /* box-shadow: 0px 1px 1px 0px var(--black-opacity-01); */ } .white-theme .menu-cont { background-color: var(--white); @@ -260,6 +264,12 @@ body.white-theme { .white-theme #progress-active { background-color: var(--black-opacity-02); } +.white-theme .a { + color: var(--gray); +} +.white-theme .mode.active { + color: var(--dark); +} /* Dark Theme */ body.dark-theme { @@ -286,6 +296,9 @@ body.dark-theme { .dark-theme .workout { border-bottom: 1px solid var(--white-opacity-01); } +.dark-theme .mode.active { + color: #fff; +} .icon-btn .icon { fill: var(--gray); @@ -361,6 +374,7 @@ body.dark-theme { justify-content: center; align-items: center; align-content: stretch; + wrap: wrap; z-index: 2; } .menu-cont.active { @@ -453,8 +467,7 @@ body.dark-theme { } .a { - color: #121212; - text-decoration: none; + color: #bbb; text-transform: uppercase; cursor: pointer; } @@ -515,17 +528,28 @@ body.dark-theme { .connection-screen { } +/* Zoom Level */ +.zoom-level-one { + /* display: none; */ +} -/* Data Screen */ -.data-screen { +.zoom-level-two { + display: none; +} + +/* Data Tiles */ +.data-tiles { width: 100%; } .data-tile { float: left; width: 33.3333%; + min-width: 30px; padding: 10px; text-align: center; } +.tile-heading { +} .tile-value { display: flex; justify-content: center; @@ -539,6 +563,106 @@ body.dark-theme { text-align: center; } +/* Data Bar */ +.data-bar { + display: flex; + /* display: none; */ + justify-content: center; + align-items: center; + position: relative; + padding: 10px 0px; + height: 150px; + color: var(--white); + /* background-color: #aaa; */ + background-color: var(--dark); + /* background-color: var(--chrome); */ + z-index: 0; +} +.data-bar-progress-cont { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: flex-end; + position: absolute; + left: 0px; + bottom: 0px; + padding: 0; + /* background-color: var(--zone-gray); */ + /* background-color: var(--zone-blue); */ + /* background-color: var(--zone-green); */ + /* background-color: var(--zone-yellow); */ + /* background-color: var(--zone-orange); */ + /* background-color: var(--zone-red); */ + z-index: 1; +} + +.data-bar-left { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 50%; + padding: 0px 0px 0px 0px; + z-index: 2; +} +.data-bar-right { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 50%; + padding: 0px 0px 0px 0px; + z-index: 2; +} +.data-bar-power { + /* width: 80px; */ + /* text-align: right; */ + text-align: center; + /* margin-bottom: 35px; */ +} +.data-bar-cadence { + /* width: 60px; */ + /* text-align: right; */ + text-align: center; + /* margin-bottom: 35px; */ +} +.data-bar-heading { + /* text-align: right; */ + width: 100%; + margin: 0 0 15px 0; + /* font-size: 18px; */ + font-size: 12px; + /* font-weight: bold; */ + text-transform: uppercase; +} +.data-bar-value { + /* width: 80px; */ + width: 100%; + text-align: right; + font-size: 48px; + font-weight: bold; + text-transform: uppercase; +} +.data-bar-target-heading { + font-size: 14px; + font-weight: bold; + text-transform: uppercase; + margin: 0 0 10px 0; +} +.data-bar-target-value { + font-size: 14px; + font-weight: bold; + text-transform: uppercase; +} + + /* Controls */ #controls { @@ -618,24 +742,25 @@ body.dark-theme { } .mode { padding: 0px 20px; - /* border: 1px solid rgba(255,255,255, 0.1); */ - color: #aaa; + color: var(--gray); + cursor: pointer; } .mode.active { - color: #fff; - /* border-bottom: 1px solid rgba(255,255,255, 0.9); */ + font-weight: bold; } .resistance-mode { + display: none; margin-bottom: 20px; } .slope-mode { - margin-bottom: 20px; + display: none; + margin-bottom: 50px; } .erg-mode { - margin-bottom: 20px; + margin-bottom: 50px; } .mode-controls { - margin-bottom: 20px; + margin-bottom: 50px; } .mode-controls header { margin: 0px auto 5px; @@ -759,6 +884,7 @@ body.dark-theme { /* Graph */ +.graphs {} .graph-screen { position: relative; height: 120px; @@ -1082,13 +1208,53 @@ body.dark-theme { font-weight: bold; } +.connections { + margin: 30px auto; +} +.connections h2 { + text-align: center; +} +.connection { + display: flex; + max-width: 480px; + margin: 0 auto; + align-items: center; + /* justify-content: center; */ + padding: 10px 10px; +} +.connection h3 {} +.connection-icon { + width: 40px; + height: 40px; + margin-right: 10px; +} +.connection-content {} + @media all and (min-width: 1800px) { } @media all and (min-width: 1600px) { } @media all and (max-width: 1250px) { } -@media all and (max-width: 980px) { +@media all and (max-width: 1024px) { + .control-screen { + height: 50px; + bottom: 49px; + } + .watch { + /* padding: 5px 0 5px; */ + padding: 10px 5px; + } + .control-btn { + margin: 0 14px; + } + .menu { + height: 50px; + /* border-top: 1px solid rgba(0,0,0,.4); */ + } + .tab-btn { + height: 50px; + } } @media all and (max-width: 768px) { } @@ -1119,12 +1285,12 @@ body.dark-theme { font-size: 10px; } - .control-screen { - height: 40px; - } + /* .control-screen { */ + /* height: 40px; */ + /* } */ .watch { /* margin: 10px 0 14px; */ - padding: 9px 5px; + /* padding: 9px 5px; */ } .devices { padding: 10px 10px 10px 10px; diff --git a/db.js b/db.js index 26d9d09..39fbb73 100644 --- a/db.js +++ b/db.js @@ -3,6 +3,8 @@ import { avgOfArray, maxOfArray, sum, mps, kph, timeDiff } from './functions.js'; import { Encode } from './fit.js'; import { FileHandler } from './file.js'; +import { IDB, Storage } from './storage.js'; +import { Session } from './session.js'; import { xf, DB } from './xf.js'; let db = DB({ @@ -24,6 +26,7 @@ let db = DB({ lapStartTime: Date.now(), workoutIntervalIndex: 0, timestamp: Date.now(), + watchState: 'stopped', inProgress: false, ftp: 0, @@ -36,15 +39,22 @@ let db = DB({ points: [], controllableFeatures: {}, + garminImportUrl: 'https://connect.garmin.com/modern/import-data', }); xf.reg('device:hr', x => db.hr = x); xf.reg('device:pwr', x => db.pwr = x); xf.reg('device:spd', x => db.spd = x); xf.reg('device:cad', x => db.cad = x); xf.reg('device:dist', x => db.distance = x); -xf.reg('watch:started', x => db.lapStartTime = Date.now()); +xf.reg('watch:started', x => { + db.lapStartTime = Date.now(); + db.watchState = 'started'; +}); +xf.reg('watch:paused', x => db.watchState = 'paused'); +xf.reg('watch:stopped', x => db.watchState = 'stopped'); xf.reg('watch:elapsed', x => db.elapsed = x); xf.reg('watch:lapTime', x => db.lapTime = x); +xf.reg('watch:stepTime', x => db.stepTime = x); xf.reg('ui:target-pwr', x => db.targetPwr = x); xf.reg('ui:ftp', x => db.ftp = x); xf.reg('storage:ftp', x => db.ftp = x); @@ -53,7 +63,7 @@ xf.reg('storage:weight', x => db.weight = x); xf.reg('ui:workoutFile', x => db.workoutFile = x); xf.reg('ui:workout:set', x => db.workout = db.workouts[x]); xf.reg('workout:add', x => db.workouts.push(x)); -xf.reg('watch:elapsed', watchTime => { +xf.reg('watch:elapsed', x => { db.distance += 1 * mps(db.spd); let record = { timestamp: Date.now(), power: db.pwr, @@ -89,11 +99,6 @@ xf.reg('watch:nextWorkoutStep', step => { db.workoutStepIndex = step; db.targetPwr = targetPwr; }); -xf.sub('ui:activity:save', x => { - let activity = Encode({data: db.records, laps: db.laps}); - let fileHandler = new FileHandler(); - fileHandler.downloadActivity(activity); -}); xf.reg('ui:resistance-target', x => db.resistanceTarget = x); xf.reg('ui:slope-target', x => db.slopeTarget = x); xf.reg('ui:tab', i => db.tab = i ); @@ -101,5 +106,41 @@ xf.reg('device:features', x => { console.log('controllable:features'); db.controllableFeatures = x; }); +xf.sub('ui:activity:save', x => { + let activity = Encode({data: db.records, laps: db.laps}); + let fileHandler = new FileHandler(); + fileHandler.downloadActivity(activity); +}); + + +// let storage = new Storage(); +let idb = new IDB(); +let session = {}; + +xf.reg('app:start', async function (x) { + await idb.open('store', 1, 'session'); + session = new Session({idb: idb}); + await session.restore(); + +}); + +xf.reg('lock:beforeunload', e => { + session.backup(idb, db); +}); +xf.reg('lock:release', e => { + session.backup(idb, db); +}); +xf.reg(`session:restore`, session => { + + for(let prop in session) { + if (session.hasOwnProperty(prop)) { + db[prop] = session[prop]; + } + } + // db.controllable = session.controllable; + // db.hrm = session.hrm; + console.log(session); +}); + export { db }; diff --git a/dom.js b/dom.js index d69995a..6e85293 100644 --- a/dom.js +++ b/dom.js @@ -1,86 +1,12 @@ import { q } from './q.js'; let dom = { - hrbConnectionScreen: { - switchBtn: q.get('#hrb-connection-btn'), - indicator: q.get('#hrb-connection-btn .indicator'), - }, - controllableConnectionScreen: { - switchBtn: q.get('#controllable-connection-btn'), - indicator: q.get('#controllable-connection-btn .indicator'), - }, - hrbSettings: { - switchBtn: q.get('#hrb-settings-btn'), - indicator: q.get('#hrb-settings-btn .indicator'), - name: q.get('#hrb-settings-name'), - manufacturer: q.get('#hrb-settings-manufacturer'), - model: q.get('#hrb-settings-model'), - firmware: q.get('#hrb-settings-firmware'), - value: q.get('#hrb-settings-value'), - battery: q.get('#hrb-settings-battery'), - }, - controllableSettings: { - switchBtn: q.get('#controllable-settings-btn'), - indicator: q.get('#controllable-settings-btn .indicator'), - name: q.get('#controllable-settings-name'), - manufacturer: q.get('#controllable-settings-manufacturer'), - model: q.get('#controllable-settings-model'), - firmware: q.get('#controllable-settings-firmware'), - power: q.get('#controllable-settings-power'), - cadence: q.get('#controllable-settings-cadence'), - speed: q.get('#controllable-settings-speed'), - }, - datascreen: { - time: q.get('#time'), - interval: q.get('#interval-time'), - targetPwr: q.get('#target-power'), - power: q.get('#power'), - cadence: q.get('#cadence'), - speed: q.get('#speed'), - distance: q.get('#distance'), - heartRate: q.get('#heart-rate') - }, - watch: { - start: q.get('#watch-start'), - pause: q.get('#watch-pause'), - // resume: q.get('#watch-resume'), - lap: q.get('#watch-lap'), - stop: q.get('#watch-stop'), - save: q.get('#activity-save'), - workout: q.get('#start-workout'), - cont: q.get('#watch'), - name: q.get('#workout-name'), - }, - controls: { - resistanceMode: q.get('#resistance-mode-btn'), - slopeMode: q.get('#slope-mode-btn'), - ergMode: q.get('#erg-mode-btn'), - freeMode: q.get('#free-mode-btn'), - - resistanceControls: q.get('#resistance-mode-controls'), - slopeControls: q.get('#slope-mode-controls'), - ergControls: q.get('#erg-mode-controls'), - - resistanceParams: q.get('#resistance-mode-params'), - slopeParams: q.get('#slope-mode-params'), - ergParams: q.get('#erg-mode-params'), - - resistanceValue: q.get('#resistance-value'), - resistanceInc: q.get('#resistance-inc'), - resistanceDec: q.get('#resistance-dec'), - // resistanceSet: q.get('#resistance-set'), - slopeValue: q.get('#slope-value'), - slopeInc: q.get('#slope-inc'), - slopeDec: q.get('#slope-dec'), - // slopeSet: q.get('#slope-set'), - - - targetPower: q.get('#target-power-value'), - workPower: q.get('#work-power-value'), - restPower: q.get('#rest-power-value'), - setTargetPower: q.get('#set-target-power'), - startWorkInterval: q.get('#start-work-interval'), - startRestInterval: q.get('#start-rest-interval'), + graphWorkout: { + // progress: q.get('#progress'), + name: q.get('#current-workout-name'), + graph: q.get('#current-workout-graph'), + intervals: [], + steps: [], }, settings: { ftp: q.get('#ftp-value'), @@ -113,22 +39,6 @@ let dom = { activity: { saveBtn: q.get('#activity-save'), }, - graphWorkout: { - // progress: q.get('#progress'), - name: q.get('#current-workout-name'), - graph: q.get('#current-workout-graph'), - intervals: [], - steps: [], - }, - // graphHr: { - // cont: q.get('#graph-hr'), - // graph: q.get('#graph-hr .graph') - // }, - graphPower: { - cont: q.get('#graph-power'), - graph: q.get('#graph-power .graph'), - ftp: q.get('#ftp-line-value') - }, // recon: { // section: q.get('#recon-cont'), // cont: q.get('#recon-graph'), diff --git a/functions.js b/functions.js index 837bfec..658071b 100644 --- a/functions.js +++ b/functions.js @@ -12,11 +12,14 @@ let avg = (x, y) => (x + y) / 2; let last = xs => xs[xs.length - 1]; let first = xs => xs[0]; let second = xs => xs[1]; +let third = xs => xs[2]; let format = (x, precision = 1000) => round(x * precision) / precision; let mps = kph => format(kph / 3.6); let kph = mps => 3.6 * mps; let nextToLast = xs => xs[xs.length - 2]; +const rand = (min = 0, max = 10) => Math.floor(Math.random() * (max - min + 1) + min); + function avgOfArray(xs, prop = false) { if(prop !== false) { return xs.reduce( (acc,v,i) => acc+(v[prop]-acc)/(i+1), 0); @@ -178,11 +181,13 @@ export { mps, kph, avg, + rand, avgOfArray, maxOfArray, sum, first, second, + third, last, nextToLast, round, diff --git a/images/connections/garmin-connect.jpg b/images/connections/garmin-connect.jpg new file mode 100644 index 0000000..8efd912 Binary files /dev/null and b/images/connections/garmin-connect.jpg differ diff --git a/index.html b/index.html index 038df92..97343e9 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ -
+
-
-
-

Power

-
-
--
+ + +
+
+
+

Power

+
+
--
+
-
-
-

Interval Time

-
-
--:--
+
+

Interval Time

+
+
--:--
+
-
-
-

Heart Rate

-
-
--
+
+

Heart Rate

+
+
--
+
-
-
-

Target

-
-
--
+ +
+

Target

+
+
--
+
+
+
+

Elapsed Time

+
+
--:--:--
+
+
+
+

Cadence

+
+
--
+
-
-

Elapsed Time

-
-
--:--:--
+
+
+

Speed

+
+
--
+
+
+
+

Distance

+
+
--
+
-
-

Cadence

-
-
--
+ +
+ +
+
+

Power

+ +
+
+
-
-
-

Speed

-
-
--
+ + + +
+
+
+
+
+

Watt

+
--
+
-
-
-

Distance

-
-
--
+
+
+

RPM

+
--
+
-
-
-
-
-

Power

-
FTP
-
+
+
+

Target

+
+
--
+
+
+
+

Interval Time

+
+
--:--
+
+
+ +
+

Heart Rate

+
+
--
+
+ +
+ +
+

Elapsed Time

+
+
--:--:--
+
+
+ +
-
+ +
@@ -112,7 +170,6 @@

ERG
Resistance
Slope
-
Free
@@ -123,7 +180,7 @@

+ type="number" value="0" autocomplete="off" tabindex="-1"/>
@@ -137,7 +194,7 @@

+ type="number" value="0" autocomplete="off" tabindex="-1"/>
@@ -150,15 +207,15 @@

- +
- +
- +
@@ -273,9 +330,20 @@

Battery

+
+

Connections

+
+ GC + +
+
+
Free ride
diff --git a/index.js b/index.js index feb881a..f1366d2 100644 --- a/index.js +++ b/index.js @@ -9,12 +9,12 @@ import { FileHandler } from './file.js'; import { StopWatch } from './workout.js'; import { WakeLock } from './lock.js'; import { workouts } from './workouts/workouts.js'; -import { Storage } from './storage.js'; import { ControllableConnectionView, HrbConnectionView, ControllableSettingsView, HrbSettingsView, DataScreen, + DataBar, GraphHr, GraphPower, GraphWorkout, @@ -31,31 +31,33 @@ import { DeviceController, WorkoutController, Screen, Vibrate } from './controllers.js'; +import { IDB, Storage } from './storage.js'; import { DataMock } from './test/mock.js'; - 'use strict'; -function start() { + +async function start() { let hrb = new Hrb({name: 'hrb'}); let flux = new Controllable({name: 'controllable'}); let watch = new StopWatch(); let lock = new WakeLock(); - ControllableConnectionView({dom: dom.controllableConnectionScreen}); - HrbConnectionView({dom: dom.hrbConnectionScreen}); + ControllableConnectionView(); + HrbConnectionView(); - ControllableConnectionView({dom: dom.controllableSettings}); - HrbConnectionView({dom: dom.hrbSettings}); + ControllableConnectionView(); + HrbConnectionView(); - ControllableSettingsView({dom: dom.controllableSettings, name: 'controllable'}); - HrbSettingsView({dom: dom.hrbSettings, name: 'hrb'}); + ControllableSettingsView({name: 'controllable'}); + HrbSettingsView({name: 'hrb'}); - DataScreen({dom: dom.datascreen}); - GraphPower({dom: dom.graphPower}); + DataScreen(); + // DataBar(); + GraphPower(); GraphWorkout({dom: dom.graphWorkout}); - WatchView({dom: dom.watch}); + WatchView(); ControlView({dom: dom.controls}); LoadWorkoutView({dom: dom.file}); WorkoutsView({dom: dom.workouts, workouts: workouts}); @@ -68,7 +70,7 @@ function start() { WorkoutController(); Screen(); - // Session(); + let storage = new Storage(); xf.dispatch('app:start'); diff --git a/lock.js b/lock.js index ff02bdb..9e4f87c 100644 --- a/lock.js +++ b/lock.js @@ -1,3 +1,5 @@ +import { xf } from './xf.js'; + class WakeLock { constructor(args) { this.lock = undefined; @@ -14,6 +16,10 @@ class WakeLock { self.lockScreen(); document.addEventListener('visibilitychange', self.onVisibilityChange.bind(self)); + + window.addEventListener('beforeunload', e => { + xf.dispatch('lock:beforeunload'); + }); } checkVisibility() { let isVisible = false; @@ -43,6 +49,7 @@ class WakeLock { lock.addEventListener('release', e => { self.isLocked = false; + xf.dispatch('lock:release'); console.log(`Wake lock released.`); }); } diff --git a/session.js b/session.js new file mode 100644 index 0000000..e66c9fa --- /dev/null +++ b/session.js @@ -0,0 +1,76 @@ +import { xf } from './xf.js'; +import { first, second, third, last } from './functions.js'; + +class Session { + constructor(args) { + this.idb = args.idb || {}; + this.data = {}; + this.init(); + } + init() { + let self = this; + + // xf.reg('db:elapsed', elapsed => self.data.elapsed); + + // xf.sub('lock:release', e => { + // // console.log(db); + // }); + + // xf.sub('lock:beforeunload', e => { + // // backupSession(idb, data); + // }); + } + async restore(idb) { + let self = this; + let sessions = await self.idb.getAll(self.idb.db, 'session'); + // let sessions = await idb.getAll(idb.db, 'session'); + let session = last(sessions); + + if(session.hasOwnProperty('elapsed')) { + if(session.elapsed > 0) { + // restore db state + xf.dispatch(`session:restore`, session); + // restore components state + xf.dispatch(`session:watchRestore`, session); + console.log('dispatch session:restore'); + } else { + console.log('dispatch session:clear'); + self.clear(self.idb); + } + } + console.log(`sessions`); + console.log(sessions); + } + async backup(idb, db) { + let self = this; + let session = self.dbToSession(db); + idb.put(idb.db, 'session', session); + } + async clear(idb) { + let self = this; + idb.clearEntries(idb.db, 'session'); + } + + dbToSession(db) { + let session = { + id: 0, + elapsed: db.elapsed, + lapTime: db.lapTime, + stepTime: db.stepTime, + targetPwr: db.targetPwr, + records: db.records, + workoutStepIndex: db.workoutStepIndex, + workoutIntervalIndex: db.workoutIntervalIndex, + }; + + console.log(session); + return session; + } + // sessionToDb(db, session) { + // // db.records = session.records; + // db.elapsed = session.elapsed; + // // db.target = session.target; + // } +} + +export { Session }; diff --git a/storage.js b/storage.js index 7dc45ae..241a8d6 100644 --- a/storage.js +++ b/storage.js @@ -1,5 +1,9 @@ import { xf } from './xf.js'; +const types = { + transaction: ['readonly', 'readwrite', 'versionchange'], +}; + class IDB { constructor() { this.db = undefined; @@ -7,97 +11,124 @@ class IDB { } init() { let self = this; - xf.sub('idb:open:success', e => { - console.log(`idb:open:success ${e.detail.data}`); - self.db = e.detail.data; + xf.sub('idb:open-success', idb => { + console.log(`idb:open-success`); + self.db = idb; }); - xf.sub('idb:delete:success', e => { - console.log(`idb:delete:success ${e.detail.data}`); - self.db = e.detail.data; - }); - xf.sub('idb:create:success', e => { - console.log(`idb:create:success ${e.detail.data}`); - // self.db = e.detail.data; + xf.sub('idb:open-error', e => { + console.warn(`idb:open-error`); }); } - open(name, version) { + open(name, version, storeName = '') { let self = this; + console.log(`idb:open ${name}:${storeName} ...`); let openReq = window.indexedDB.open(name, version); - openReq.onupgradeneeded = function(e) { - let idb = openReq.result; - switch(e.oldVersion) { - case 0: self.createStore(idb, name); + return new Promise((resolve, reject) => { + openReq.onupgradeneeded = function(e) { + let idb = openReq.result; + switch(e.oldVersion) { + case 0: self.createStore(idb, storeName); case 1: self.update(idb); - } - }; - openReq.onerror = function() { - console.error(`idb open error: ${openReq.error}`); - xf.dispatch('idb:open:error'); - }; - openReq.onsuccess = function() { - let idb = openReq.result; - xf.dispatch('idb:open:success', idb); - }; - } - delete(name) { - let deleteReq = indexedDB.deleteDatabase(name); - deleteReq.onerror = function() { - console.error(`idb delete error: ${deleteReq.error}`); - }; - deleteReq.onsuccess = function() { - let res = deleteReq.result; - xf.dispatch('idb:delete:success', res); - }; + } + }; + openReq.onerror = function() { + console.error(`idb open error: ${openReq.error}`); + xf.dispatch('idb:open-error'); + return reject(openReq.error); + }; + openReq.onsuccess = function() { + let idb = openReq.result; + xf.dispatch('idb:open-success', idb); + return resolve(openReq.result); + }; + }); + } + delete(idb, name) { + let self = this; + let deleteReq = idb.deleteDatabase(name); + + return self.promisify(deleteReq).then(res => { + console.log(`idb delete ${name} success`); + return res; + }).catch(err => { + console.error(`idb delete ${name} error: ${err}`); + return {}; + }); } update(idb) { - xf.dispatch('idb:update:success', idb); + let self = this; + xf.dispatch('idb:update-success', idb); return idb; } createStore(idb, name) { + let self = this; if (!idb.objectStoreNames.contains(name)) { idb.createObjectStore(name, {keyPath: 'id'}); - xf.dispatch('idb:create:success', idb); + xf.dispatch('idb:create-success', idb); } else { console.error(`idb trying to create store with existing name: ${name}`); } } - add(idb, storeName, item, type = 'readonly') { - let transaction = idb.transaction(storeName, type); - let store = transaction.objectStore(storeName); - let addReq = store.add(item); - addReq.onsuccess = function() { - let res = addReq.result; - console.log(`idb add success: ${res}`); - }; - addReq.onerror = function() { - let err = addReq.error; - console.error(`idb add error: ${err}, , store: ${storeName} item: ${item}`); - }; - transaction.oncomplete = function() { - let res = transaction; - console.log(`idb transaction complete: ${res}`); - }; - } - put(idb, storeName, item, type = 'readonly') { + add(idb, storeName, item) { + let self = this; + return self.transaction(idb, storeName, 'add', item, 'readwrite'); + } + put(idb, storeName, item) { + let self = this; + return self.transaction(idb, storeName, 'put', item, 'readwrite'); + } + get(idb, storeName, key) { + let self = this; + return self.transaction(idb, storeName, 'get', key, 'readonly'); + } + getAll(idb, storeName) { + let self = this; + return self.transaction(idb, storeName, 'getAll', undefined, 'readonly'); + } + deleteEntry(idb, storeName, id) { + let self = this; + return self.transaction(idb, storeName, 'delete', id, 'readwrite'); + } + clearEntries(idb, storeName) { + let self = this; + return self.transaction(idb, storeName, 'clear', undefined, 'readwrite'); + } + transaction(idb, storeName, method, param = undefined, type = 'readonly') { + let self = this; let transaction = idb.transaction(storeName, type); let store = transaction.objectStore(storeName); - let addReq = store.add(item); - addReq.onsuccess = function() { - let res = addReq.result; - console.log(`idb put success: ${res}`); - }; - addReq.onerror = function() { - let err = addReq.error; - console.error(`idb put error: ${err}, , store: ${storeName} item: ${item}`); - }; + let req; + // console.log(`${storeName}: ${method}`); + // console.log(transaction); + // console.log(store); + + if(param === undefined) { + req = store[method](); + } else { + req = store[method](param); + } + + return self.promisify(req).then(res => { + console.log(`idb ${method} ${storeName} success`); + return res; + }).catch(err => { + console.error(`idb ${method} ${storeName} error: ${err}`); + return []; + }); + } + promisify(request) { + return new Promise((resolve, reject) => { + request.onsuccess = function(event) { + return resolve(request.result); + }; + request.onerror = function(event) { + return reject(request.error); + }; + }); } } -let types = { - transaction: ['readonly', 'readwrite', 'versionchange'], -}; - class Storage { constructor(){ this.init(); @@ -120,16 +151,14 @@ class Storage { xf.dispatch('storage:ftp', parseInt(ftp)); xf.dispatch('storage:weight', parseInt(weight)); - xf.sub('ui:ftp', e => { - self.setFtp(parseInt(e.detail.data)); + xf.sub('ui:ftp', ftp => { + self.setFtp(parseInt(ftp)); }); - xf.sub('ui:weight', e => { - self.setWeight(parseInt(e.detail.data)); + xf.sub('ui:weight', weight => { + self.setWeight(parseInt(weight)); }); } - open() { - } setFtp(ftp) { if(isNaN(ftp) || ftp > 600 || ftp < 30) { console.warn(`Trying to enter Invalid FTP value in Storage: ${ftp}`); @@ -171,4 +200,4 @@ class Storage { } } -export { Storage }; +export { Storage, IDB }; diff --git a/test/mock.js b/test/mock.js index 4b864c5..cef0b42 100644 --- a/test/mock.js +++ b/test/mock.js @@ -4,6 +4,7 @@ import { avgOfArray, sum, mps, kph, + rand, first, last, round, @@ -12,11 +13,17 @@ import { avgOfArray, function DataMock(args) { let count = 0; let interval = null; + let power = 0; + + xf.sub('db:targetPwr', pwr => { + power = pwr; + }); xf.sub('watch:started', e => { interval = setInterval(function() { - let power = (count % 60) < 30 ? 100 : 300; + // let power = (count % 60) < 30 ? 100 : 300; + let hr = (count % 60) < 30 ? 120 : 160; let cadence = (count % 60) < 30 ? 75 : 90; let speed = (count % 60) < 30 ? 27.0 : 39.0; @@ -26,13 +33,14 @@ function DataMock(args) { xf.dispatch('device:hr', hr); } if(args.pwr) { - xf.dispatch('device:pwr', power); + xf.dispatch('device:pwr', power + rand(-10, 10)); xf.dispatch('device:cad', cadence); xf.dispatch('device:spd', speed); // xf.dispatch('device:dist', distance); } + console.log(`mock ${power}`); count += 1; - }, 1000); + }, 700); }); xf.sub('watch:stopped', e => { diff --git a/views.js b/views.js index 66e59bf..dbc6c75 100644 --- a/views.js +++ b/views.js @@ -1,4 +1,5 @@ import { xf } from './xf.js'; +import { q } from './q.js'; import { avgOfArray, hrToColor, powerToZone, @@ -8,7 +9,11 @@ import { avgOfArray, import { parseZwo, intervalsToGraph } from './parser.js'; function ControllableConnectionView(args) { - let dom = args.dom; + let dom = { + switchBtn: q.get('#controllable-connection-btn'), + indicator: q.get('#controllable-connection-btn .indicator'), + }; + xf.sub('pointerup', e => xf.dispatch('ui:controllableSwitch'), dom.switchBtn); xf.sub('controllable:connected', e => { @@ -23,7 +28,11 @@ function ControllableConnectionView(args) { } function HrbConnectionView(args) { - let dom = args.dom; + let dom = { + switchBtn: q.get('#hrb-connection-btn'), + indicator: q.get('#hrb-connection-btn .indicator'), + }; + xf.sub('pointerup', e => xf.dispatch('ui:hrbSwitch'), dom.switchBtn); xf.sub('hrb:connected', e => { @@ -38,37 +47,87 @@ function HrbConnectionView(args) { } function DataScreen(args) { - let dom = args.dom; - xf.sub('db:hr', e => { - let hr = e.detail.data.hr; + let dom = { + time: q.get('#time'), + interval: q.get('#interval-time'), + targetPwr: q.get('#target-power'), + power: q.get('#power'), + cadence: q.get('#cadence'), + speed: q.get('#speed'), + distance: q.get('#distance'), + heartRate: q.get('#heart-rate') + }; + + xf.sub('db:hr', hr => { dom.heartRate.textContent = `${hr}`; }); - xf.sub('db:pwr', e => { - let pwr = e.detail.data.pwr; + xf.sub('db:pwr', pwr => { dom.power.textContent = `${pwr}`; }); - xf.sub('db:distance', e => { - let dis = e.detail.data.distance; - dom.distance.textContent = `${metersToDistance(dis)}`; + xf.sub('db:distance', distance => { + dom.distance.textContent = `${metersToDistance(distance)}`; }); - xf.sub('db:vspd', e => { - let vspd = e.detail.data.vspd; + xf.sub('db:vspd', vspd => { dom.speed.textContent = `${vspd.toFixed(1)}`; }); - xf.sub('db:spd', e => { - let spd = e.detail.data.spd; + xf.sub('db:spd', spd => { dom.speed.textContent = `${spd.toFixed(1)}`; }); - xf.sub('db:cad', e => { - let cad = e.detail.data.cad; + xf.sub('db:cad', cad => { + dom.cadence.textContent = `${cad}`; + }); + xf.sub('db:elapsed', elapsed => { + dom.time.textContent = secondsToHms(elapsed); + }); + xf.sub('db:lapTime', lapTime => { + if(!Number.isInteger(lapTime)) { + lapTime = 0; + } + if(lapTime < 0) { + lapTime = 0; + } + dom.interval.textContent = secondsToHms(lapTime, true); + }); + xf.sub('db:targetPwr', targetPwr => { + dom.targetPwr.textContent = targetPwr; + }); +} + +function DataBar(args) { + let dom = { + time: q.get('#data-bar-time'), + interval: q.get('#data-bar-interval-time'), + targetPwr: q.get('#data-bar-target-power'), + power: q.get('#data-bar-power'), + cadence: q.get('#data-bar-cadence'), + heartRate: q.get('#data-bar-heart-rate'), + progress: q.get('#data-bar-progress-cont'), + }; + let ftp = 250; + + xf.sub('db:hr', hr => { + // let hr = e.detail.data.hr; + dom.heartRate.textContent = `${hr}`; + }); + xf.sub('db:pwr', pwr => { + // let pwr = e.detail.data.pwr; + dom.power.textContent = `${pwr}`; + dom.progress.insertAdjacentHTML('beforeend', `
`); + }); + xf.sub('db:vspd', vspd => { + // let vspd = e.detail.data.vspd; + dom.speed.textContent = `${vspd.toFixed(1)}`; + }); + xf.sub('db:cad', cad => { + // let cad = e.detail.data.cad; dom.cadence.textContent = `${cad}`; }); - xf.sub('db:elapsed', e => { - let elapsed = e.detail.data.elapsed; + xf.sub('db:elapsed', elapsed => { + // let elapsed = e.detail.data.elapsed; dom.time.textContent = secondsToHms(elapsed); }); - xf.sub('db:lapTime', e => { - let lapTime = e.detail.data.lapTime; + xf.sub('db:lapTime', lapTime => { + // let lapTime = e.detail.data.lapTime; if(!Number.isInteger(lapTime)) { lapTime = 0; } @@ -77,74 +136,117 @@ function DataScreen(args) { } dom.interval.textContent = secondsToHms(lapTime, true); }); - xf.sub('db:targetPwr', e => { - dom.targetPwr.textContent = e.detail.data.targetPwr; + xf.sub('db:targetPwr', targetPwr => { + // dom.targetPwr.textContent = e.detail.data.targetPwr; + dom.targetPwr.textContent = targetPwr; }); } function ControllableSettingsView(args) { - let dom = args.dom; let name = args.name || 'controllable'; + let dom = { + switchBtn: q.get('#controllable-settings-btn'), + indicator: q.get('#controllable-settings-btn .indicator'), + name: q.get('#controllable-settings-name'), + manufacturer: q.get('#controllable-settings-manufacturer'), + model: q.get('#controllable-settings-model'), + firmware: q.get('#controllable-settings-firmware'), + power: q.get('#controllable-settings-power'), + cadence: q.get('#controllable-settings-cadence'), + speed: q.get('#controllable-settings-speed'), + }; - xf.sub('db:pwr', e => { - let power = e.detail.data.pwr; - dom.power.textContent = `${power}`; + xf.sub('db:pwr', pwr => { + // let power = e.detail.data.pwr; + dom.power.textContent = `${pwr}`; }); - xf.sub('db:cad', e => { - let cadence = e.detail.data.cad; - dom.cadence.textContent = `${cadence}`; + xf.sub('db:cad', cad => { + // let cadence = e.detail.data.cad; + dom.cadence.textContent = `${cad}`; }); - xf.sub('db:spd', e => { - let speed = e.detail.data.spd; - dom.speed.textContent = `${speed}`; + xf.sub('db:spd', spd => { + // let speed = e.detail.data.spd; + dom.speed.textContent = `${spd}`; }); - xf.sub(`${name}:info`, e => { - console.log(e.detail.data); - dom.name.textContent = `${e.detail.data.name}`; - dom.model.textContent = `${e.detail.data.modelNumberString}`; - dom.manufacturer.textContent = `${e.detail.data.manufacturerNameString}`; - dom.firmware.textContent = `${e.detail.data.firmwareRevisionString}`; + xf.sub(`${name}:info`, data => { + console.log(data); + dom.name.textContent = `${data.name}`; + dom.model.textContent = `${data.modelNumberString}`; + dom.manufacturer.textContent = `${data.manufacturerNameString}`; + dom.firmware.textContent = `${data.firmwareRevisionString}`; }); } function HrbSettingsView(args) { - let dom = args.dom; let name = args.name || 'hrb'; + let dom = { + switchBtn: q.get('#hrb-settings-btn'), + indicator: q.get('#hrb-settings-btn .indicator'), + name: q.get('#hrb-settings-name'), + manufacturer: q.get('#hrb-settings-manufacturer'), + model: q.get('#hrb-settings-model'), + firmware: q.get('#hrb-settings-firmware'), + value: q.get('#hrb-settings-value'), + battery: q.get('#hrb-settings-battery'), + }; - xf.sub('db:hr', e => { - let hr = e.detail.data.hr; + xf.sub('db:hr', hr => { + // let hr = e.detail.data.hr; dom.value.textContent = `${hr} bpm`; }); - xf.sub(`${name}:info`, e => { - console.log(e.detail.data); - dom.name.textContent = `${e.detail.data.name}`; - dom.model.textContent = `${e.detail.data.modelNumberString}`; - dom.manufacturer.textContent = `${e.detail.data.manufacturerNameString}`; - dom.firmware.textContent = `${e.detail.data.firmwareRevisionString}`; + xf.sub(`${name}:info`, data => { + console.log(data); + dom.name.textContent = `${data.name}`; + dom.model.textContent = `${data.modelNumberString}`; + dom.manufacturer.textContent = `${data.manufacturerNameString}`; + dom.firmware.textContent = `${data.firmwareRevisionString}`; }); } function GraphPower(args) { - let dom = args.dom; + let dom = { + cont: q.get('#graph-power'), + graph: q.get('#graph-power .graph'), + // ftp: q.get('#ftp-line-value') + }; let ftp = 100; let size = dom.cont.getBoundingClientRect().width; let count = 0; let scale = 400; + let workout = {}; + let intervalIndex = 0; + let width = 1; - xf.sub('db:ftp', e => { - ftp = e.detail.data.ftp; - dom.ftp.textContent = `FTP ${ftp}`; + xf.sub('db:ftp', x => { + // dom.ftp.textContent = `FTP ${x}`; + ftp = x; }); - xf.sub('db:pwr', e => { - let pwr = e.detail.data.pwr; + + xf.reg('db:elapsed', db => { + let pwr = db.pwr; let h = valueToHeight(scale, pwr); - count += 1; - if(count >= size) { - dom.graph.removeChild(dom.graph.childNodes[0]); - } - dom.graph.insertAdjacentHTML('beforeend', `
`); + dom.graph.insertAdjacentHTML('beforeend', `
`); + }); + + // xf.sub('db:pwr', pwr => { + // let h = valueToHeight(scale, pwr); + // count += 1; + // if(count >= size) { + // dom.graph.removeChild(dom.graph.childNodes[0]); + // } + // dom.graph.insertAdjacentHTML('beforeend', `
`); + // }); + + xf.sub('watch:nextWorkoutInterval', index => { + dom.graph.innerHTML = ''; + intervalIndex = index; + width = size / workout.intervals[intervalIndex].duration; + }); + + xf.sub('db:workout', x => { + workout = x; }); } @@ -153,8 +255,8 @@ function GraphHr(args) { let count = 0; let scale = 200; let size = dom.cont.getBoundingClientRect().width; - xf.sub('db:hr', e => { - let hr = e.detail.data.hr; + xf.sub('db:hr', hr => { + // let hr = e.detail.data.hr; let h = valueToHeight(scale, hr); count += 1; if(count >= size) { @@ -214,7 +316,7 @@ function NavigationWidget(args) { e.preventDefault(); e.stopImmediatePropagation(); - i = dom.homeBtn.getAttribute('date-index'); + i = dom.homeBtn.getAttribute('data-index'); dom.homePage.style.display = 'block'; dom.settingsPage.style.display = 'none'; dom.workoutsPage.style.display = 'none'; @@ -234,7 +336,7 @@ function NavigationWidget(args) { e.preventDefault(); e.stopImmediatePropagation(); - i = dom.settingsBtn.getAttribute('date-index'); + i = dom.settingsBtn.getAttribute('data-index'); dom.settingsPage.style.display = 'block'; dom.homePage.style.display = 'none'; dom.workoutsPage.style.display = 'none'; @@ -254,7 +356,7 @@ function NavigationWidget(args) { e.preventDefault(); e.stopImmediatePropagation(); - i = dom.workoutsBtn.getAttribute('date-index'); + i = dom.workoutsBtn.getAttribute('data-index'); dom.workoutsPage.style.display = 'block'; dom.homePage.style.display = 'none'; dom.settingsPage.style.display = 'none'; @@ -276,12 +378,12 @@ function SettingsView(args) { let ftp = 100; let weight = 75; - xf.sub('db:ftp', e => { - ftp = e.detail.data.ftp; + xf.sub('db:ftp', ftp => { + // ftp = e.detail.data.ftp; dom.ftp.value = ftp; }); - xf.sub('db:weight', e => { - weight = e.detail.data.weight; + xf.sub('db:weight', weight => { + // weight = e.detail.data.weight; dom.weight.value = weight; }); @@ -297,14 +399,136 @@ function SettingsView(args) { }, dom.weightBtn); } +function NumberInput(args) { + let dom = args.dom; + let name = args.name || `number-input`; + let value = args.init || 0; + let type = args.type || 'Int'; + let minValueSupported = args.min || 0; + let maxValueSupported = args.max || 0; + let incStep = args.inc || 1; + let set = args.set || function(x) { return x; }; + + const inRange = (target, minValueSupported, maxValueSupported, init = 0) => { + let value = init; + if(target >= maxValueSupported) { + value = maxValueSupported; + } else if(target < minValueSupported) { + value = minValueSupported; + } else { + value = target; + } + return value; + }; + + xf.sub('change', e => { + let x = 0; + if(type === 'Int') { + x = parseInt(e.target.value || 0); + } else { + x = parseFloat(e.target.value || 0); + } + if(x > minValueSupported && x < maxValueSupported) { + value = x; + } + if(x >= maxValueSupported) { + value = maxValueSupported; + } + if(x <= minValueSupported) { + value = minValueSupported; + } + dom.input.value = value; + set(value); + xf.dispatch(`ui:${name}-target`, value); + }, dom.input); + + xf.sub('pointerup', e => { + let target = value + incStep; + value = inRange(target, minValueSupported, maxValueSupported, value); + dom.input.value = value; + set(value); + xf.dispatch(`ui:${name}-target`, value); + }, dom.incBtn); + + xf.sub('pointerup', e => { + let target = value - incStep; + value = inRange(target, minValueSupported, maxValueSupported, value); + dom.input.value = value; + set(value); + xf.dispatch(`ui:${name}-target`, value); + }, dom.decBtn); +} + function ControlView(args) { - let dom = args.dom; + let dom = { + resistanceModeBtn: q.get('#resistance-mode-btn'), + slopeModeBtn: q.get('#slope-mode-btn'), + ergModeBtn: q.get('#erg-mode-btn'), + + resistanceControls: q.get('#resistance-mode-controls'), + slopeControls: q.get('#slope-mode-controls'), + ergControls: q.get('#erg-mode-controls'), + + resistanceParams: q.get('#resistance-mode-params'), + slopeParams: q.get('#slope-mode-params'), + ergParams: q.get('#erg-mode-params'), + + resistanceValue: q.get('#resistance-value'), + resistanceInc: q.get('#resistance-inc'), + resistanceDec: q.get('#resistance-dec'), + + slopeValue: q.get('#slope-value'), + slopeInc: q.get('#slope-inc'), + slopeDec: q.get('#slope-dec'), + + targetPower: q.get('#target-power-value'), + workPower: q.get('#work-power-value'), + restPower: q.get('#rest-power-value'), + setTargetPower: q.get('#set-target-power'), + startWorkInterval: q.get('#start-work-interval'), + startRestInterval: q.get('#start-rest-interval'), + }; - xf.sub('db:controllableFeatures', e => { - let features = e.detail.data.controllableFeatures; - Resistance(features); - Slope(features); - ERG(features); + xf.sub('pointerup', e => { + xf.dispatch('ui:erg-mode'); + dom.ergModeBtn.classList.add('active'); + dom.resistanceModeBtn.classList.remove('active'); + dom.slopeModeBtn.classList.remove('active'); + dom.ergControls.style.display = 'block'; + dom.resistanceControls.style.display = 'none'; + dom.slopeControls.style.display = 'none'; + }, dom.ergModeBtn); + + xf.sub('pointerup', e => { + xf.dispatch('ui:resistance-mode'); + dom.ergModeBtn.classList.remove('active'); + dom.resistanceModeBtn.classList.add('active'); + dom.slopeModeBtn.classList.remove('active'); + dom.ergControls.style.display = 'none'; + dom.resistanceControls.style.display = 'block'; + dom.slopeControls.style.display = 'none'; + }, dom.resistanceModeBtn); + + xf.sub('pointerup', e => { + xf.dispatch('ui:slope-mode'); + dom.ergModeBtn.classList.remove('active'); + dom.resistanceModeBtn.classList.remove('active'); + dom.slopeModeBtn.classList.add('active'); + dom.ergControls.style.display = 'none'; + dom.resistanceControls.style.display = 'none'; + dom.slopeControls.style.display = 'block'; + }, dom.slopeModeBtn); + + + // ERG({power: {params: {min: 0, max: 800, inc: 1}}}); + // Slope({slope: {params: {min: 0, max: 30, inc: 0.5}}}); + // Resistance({resistance: {params: {min: 0, max: 100, inc: 1}}}); + + xf.sub('db:controllableFeatures', controllableFeatures => { + let features = controllableFeatures; + Resistance(features); // will overflow max value + Slope(features); // speed based, will ignore max value + ERG(features); // will ignore max value (most likely) }); function Resistance(features) { @@ -316,113 +540,48 @@ function ControlView(args) { dom.resistanceParams.textContent = `${minResistanceSupported} to ${maxResistanceSupported}`; - xf.sub('change', e => { - let value = parseInt(e.target.value || 0); - if(value <= minResistanceSupported) { - resistance = minResistanceSupported; - dom.resistanceValue.value = resistance; - } - if(value >= maxResistanceSupported) { - resistance = maxResistanceSupported; - dom.resistanceValue.value = resistance; - } - if(value >= minResistanceSupported && value < maxResistanceSupported) { - resistance = value; - } - xf.dispatch('ui:resistance-target', resistance); - }, dom.resistanceValue); - - xf.sub('pointerup', e => { - let target = resistance + resistanceInc; - if(target >= maxResistanceSupported) { - resistance = maxResistanceSupported; - } else if(target < minResistanceSupported) { - resistance = minResistanceSupported; - } else { - resistance = target; - } - dom.resistanceValue.value = resistance; - xf.dispatch('ui:resistance-target', resistance); - }, dom.resistanceInc); - - xf.sub('pointerup', e => { - let target = resistance - resistanceInc; - if(target >= maxResistanceSupported) { - resistance = maxResistanceSupported; - } else if(target < 0) { - resistance = minResistanceSupported; - } else { - resistance = target; - } - dom.resistanceValue.value = resistance; - xf.dispatch('ui:resistance-target', resistance); - }, dom.resistanceDec); + NumberInput({dom: {input: dom.resistanceValue, + incBtn: dom.resistanceInc, + decBtn: dom.resistanceDec}, + name: 'resistance', + init: resistance, + type: 'Int', + min: minResistanceSupported, + max: maxResistanceSupported, + inc: resistanceInc}); } function Slope(features) { // Slope mode - // Waiting on: - // https://stackoverflow.com/questions/65257156/what-is-the-supported-range-of-indoor-bike-simulation-parameters-in-the-ftms-spe let slope = 0; - let minSlopeSupported = 0; //-10; - let maxSlopeSupported = 14.0; // change it + let minSlopeSupported = 0; + let maxSlopeSupported = 30.0; // maybe ... it is speed dependant let slopeInc = 0.5; dom.slopeParams.textContent = `${minSlopeSupported} to ${maxSlopeSupported}`; - xf.sub('change', e => { - let value = parseFloat(e.target.value || 0); - if(value > minSlopeSupported && value < maxSlopeSupported) { - slope = value; - } - if(value >= maxSlopeSupported) { - slope = maxSlopeSupported - 0; - dom.slopeValue.value = slope; - } - if(value <= minSlopeSupported) { - slope = minSlopeSupported + 0; - dom.slopeValue.value = slope; - } + xf.sub('ui:slope-mode', e => { xf.dispatch('ui:slope-target', slope); - }, dom.slopeValue); - - xf.sub('pointerup', e => { - let target = slope + slopeInc; - if(target >= maxSlopeSupported) { - slope = maxSlopeSupported; - } else if(target < minSlopeSupported) { - slope = minSlopeSupported; - } else { - slope = target; - } - dom.slopeValue.value = slope; - xf.dispatch('ui:slope-target', slope); - }, dom.slopeInc); - - xf.sub('pointerup', e => { - let target = slope - slopeInc; - if(target >= maxSlopeSupported) { - slope = maxSlopeSupported; - } else if(target < 0) { - slope = 0; - } else { - slope = target; - } - dom.slopeValue.value = slope; - xf.dispatch('ui:slope-target', slope); - }, dom.slopeDec); - - // xf.sub('pointerup', e => { - // xf.dispatch('ui:slope-target', slope); - // }, dom.slopeSet); + }); + + NumberInput({dom: {input: dom.slopeValue, + incBtn: dom.slopeInc, + decBtn: dom.slopeDec}, + name: 'slope', + init: slope, + type: 'Float', + min: minSlopeSupported, + max: maxSlopeSupported, + inc: slopeInc, + set: value => slope = value}); } // ERG mode function ERG(features) { - let targetPwr = 100; - let workPwr = 235; - let restPwr = 100; + let targetPwr = dom.targetPower.value || 100; + let workPwr = dom.workPower.value || 235; + let restPwr = dom.restPower || 100; let minPowerSupported = features.power.params.min || 0; let maxPowerSupported = features.power.params.max || 0; @@ -430,7 +589,9 @@ function ControlView(args) { dom.ergParams.textContent = `${minPowerSupported} to ${maxPowerSupported}`; - xf.sub('change', e => { targetPwr = parseInt(e.target.value); }, dom.targetPower); + xf.sub('change', e => { + alert(`change targetPwr`); + targetPwr = parseInt(e.target.value); }, dom.targetPower); xf.sub('change', e => { workPwr = parseInt(e.target.value); }, dom.workPower); xf.sub('change', e => { restPwr = parseInt(e.target.value); }, dom.restPower); @@ -451,7 +612,16 @@ function ControlView(args) { } function WatchView(args) { - let dom = args.dom; + let dom = { + start: q.get('#watch-start'), + pause: q.get('#watch-pause'), + lap: q.get('#watch-lap'), + stop: q.get('#watch-stop'), + save: q.get('#activity-save'), + workout: q.get('#start-workout'), + cont: q.get('#watch'), + name: q.get('#workout-name'), + }; xf.sub('pointerup', e => xf.dispatch('ui:watchStart'), dom.start); xf.sub('pointerup', e => xf.dispatch('ui:watchPause'), dom.pause); @@ -621,6 +791,7 @@ export { ControllableSettingsView, HrbSettingsView, DataScreen, + DataBar, GraphHr, GraphPower, GraphWorkout, diff --git a/workout.js b/workout.js index 4f3822f..0dd5cc0 100644 --- a/workout.js +++ b/workout.js @@ -66,8 +66,9 @@ class StopWatch { } init() { let self = this; - xf.sub('db:workout', e => { - self.workout = e.detail.data.workout.intervals; + xf.sub('db:workout', workout => { + // self.workout = e.detail.data.workout.intervals; + self.workout = workout.intervals; }); } start() { @@ -143,7 +144,6 @@ class StopWatch { self.stepTime = stepDuration; self.workoutCurrentStepDuration = stepDuration; - xf.dispatch('watch:nextWorkoutStep', s); xf.dispatch('watch:stepTime', stepDuration); xf.dispatch('watch:step'); @@ -190,6 +190,11 @@ class StopWatch { self.started = true; } } + restore(args) { + let self = this; + self.elapsed = args.elapsed; + self.resume(); + } stop () { let self = this; if(self.started) { diff --git a/workouts/workouts.js b/workouts/workouts.js index b8463c8..2da93ca 100644 --- a/workouts/workouts.js +++ b/workouts/workouts.js @@ -2,13 +2,13 @@ let workouts = [ { name: 'Dijon', type: 'VO2 Max', - description: '60/60s or 60 sec ON at 131% of FTP followed by 60 sec OFF. In 2 groups by 8 reps each.', + description: '60/60s or 60 sec ON at 121% of FTP followed by 60 sec OFF. In 2 groups by 8 reps each.', duration: 57, xml: ` Marinov Dijon - 60/60s or 60 sec ON at 131% of FTP followed by 60 sec OFF. In 2 groups by 8 reps each. + 60/60s or 60 sec ON at 121% of FTP followed by 60 sec OFF. In 2 groups by 8 reps each. bike @@ -20,10 +20,10 @@ let workouts = - - - - + + + + @@ -49,10 +49,10 @@ let workouts = - - - - + + + + @@ -104,13 +104,13 @@ let workouts = - + - + - - + + ` }, @@ -130,38 +130,36 @@ let workouts = - - - - + + + + ` }, -{ name: 'Maple (flat)', +{ name: 'Honey', type: 'Sweet Spot', description: '4 times 10 min Sweet Spot intervals with 5 min recovery in between.', duration: 80, xml: ` Marinov - Maple + Honey 4 times 10 min Sweet Spot intervals with 5 min recovery in between. bike - - - - - - - + + + + + ` }, { name: 'Baguette', type: 'Base', description: 'The bread and butter of Endurance training with efforts in Zone 1 and 2.', - duration: 120, + duration: 90, xml: ` Marinov @@ -171,21 +169,17 @@ let workouts = - - - - - + - - - + + + + + + + - - - - - + ` }, @@ -202,7 +196,7 @@ let workouts = - + @@ -216,7 +210,7 @@ let workouts = - + ` }, diff --git a/xf.js b/xf.js index a17e866..0514e88 100644 --- a/xf.js +++ b/xf.js @@ -1,4 +1,7 @@ -let evt = name => value => new CustomEvent(name, {detail: {data: value}}); +const evt = name => value => new CustomEvent(name, {detail: {data: value}}); +const evtSource = name => name.split(':')[0]; +const evtProp = name => name.split(':')[1]; +const dbSource = name => name.startsWith('db'); function dispatch (name, value) { document.dispatchEvent(evt(name)(value)); @@ -10,7 +13,10 @@ function sub(name, handler, el = false) { if(el) { el.addEventListener(name, e => handler(e)); } else { - document.addEventListener(name, e => handler(e)); + document.addEventListener(name, e => { + dbSource(name) ? handler(e.detail.data[evtProp(name)]) : handler(e.detail.data) ; + // handler(e); + }); } };