incyclist-services 1.7.118 → 1.7.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/activities/base/repo/db.js +20 -2
- package/lib/cjs/activities/base/utils/activity.js +2 -2
- package/lib/cjs/activities/ride/service.js +4 -4
- package/lib/cjs/devices/ride/cyclingMode.js +7 -4
- package/lib/cjs/ride/page/service.js +68 -11
- package/lib/cjs/routes/list/service.js +4 -0
- package/lib/cjs/workouts/ride/service.js +54 -0
- package/lib/esm/activities/base/repo/db.js +20 -2
- package/lib/esm/activities/base/utils/activity.js +2 -2
- package/lib/esm/activities/ride/service.js +4 -4
- package/lib/esm/devices/ride/cyclingMode.js +7 -4
- package/lib/esm/ride/page/service.js +68 -11
- package/lib/esm/routes/list/service.js +4 -0
- package/lib/esm/workouts/ride/service.js +54 -0
- package/lib/types/activities/base/repo/db.d.ts +1 -0
- package/lib/types/ride/page/service.d.ts +13 -2
- package/lib/types/ride/page/types.d.ts +18 -0
- package/lib/types/workouts/ride/service.d.ts +5 -0
- package/lib/types/workouts/ride/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -47,6 +47,7 @@ const Injection_1 = require("../../../base/decorators/Injection");
|
|
|
47
47
|
const i18n_1 = require("../../../i18n");
|
|
48
48
|
exports.DB_VERSION = '5';
|
|
49
49
|
exports.DB_NAME = 'db';
|
|
50
|
+
const SHORT_RIDE_FILTER_CUTOFF = new Date('2026-08-28').getTime();
|
|
50
51
|
let ActivitiesRepository = (() => {
|
|
51
52
|
let _classDecorators = [types_1.Singleton];
|
|
52
53
|
let _classDescriptor;
|
|
@@ -109,7 +110,7 @@ let ActivitiesRepository = (() => {
|
|
|
109
110
|
this.write();
|
|
110
111
|
}
|
|
111
112
|
if (writeDetails) {
|
|
112
|
-
this.writeDetails(activity);
|
|
113
|
+
await this.writeDetails(activity);
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
migrate(activity, writeSummary = true) {
|
|
@@ -224,6 +225,16 @@ let ActivitiesRepository = (() => {
|
|
|
224
225
|
}
|
|
225
226
|
search(criteria) {
|
|
226
227
|
let result = this.activities;
|
|
228
|
+
if (!this.isMobile()) {
|
|
229
|
+
result = result?.filter(ai => {
|
|
230
|
+
const { rideTime, startTime } = ai.summary;
|
|
231
|
+
if (rideTime < 1)
|
|
232
|
+
return false;
|
|
233
|
+
if (rideTime <= 30 && startTime < SHORT_RIDE_FILTER_CUTOFF)
|
|
234
|
+
return false;
|
|
235
|
+
return true;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
227
238
|
result = this.checkIdFilter(result, criteria);
|
|
228
239
|
result = this.checkHashFilter(result, criteria);
|
|
229
240
|
result = this.checkStartPosFilter(result, criteria);
|
|
@@ -475,7 +486,7 @@ let ActivitiesRepository = (() => {
|
|
|
475
486
|
}
|
|
476
487
|
async writeDetails(activity) {
|
|
477
488
|
const name = activity.summary.name;
|
|
478
|
-
this.getRepo().write(name, activity.details);
|
|
489
|
+
await this.getRepo().write(name, activity.details);
|
|
479
490
|
}
|
|
480
491
|
async scanForNewActivities() {
|
|
481
492
|
try {
|
|
@@ -509,6 +520,13 @@ let ActivitiesRepository = (() => {
|
|
|
509
520
|
getRouteList() {
|
|
510
521
|
return (0, routes_1.useRouteList)();
|
|
511
522
|
}
|
|
523
|
+
isMobile() {
|
|
524
|
+
const { appInfo } = this.getBindings();
|
|
525
|
+
if (!appInfo) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
return appInfo?.getChannel() === 'mobile';
|
|
529
|
+
}
|
|
512
530
|
getBindings() {
|
|
513
531
|
return (0, api_1.getBindings)();
|
|
514
532
|
}
|
|
@@ -254,7 +254,7 @@ let Activity = (() => {
|
|
|
254
254
|
isRouteAvailable() {
|
|
255
255
|
if (!this.details?.route)
|
|
256
256
|
return false;
|
|
257
|
-
if (this.details.routeType === 'Free-Ride')
|
|
257
|
+
if (this.details.routeType === 'Free-Ride' || this.details.routeType === 'None')
|
|
258
258
|
return false;
|
|
259
259
|
return this.getRouteList().getRouteDescription(this.details.route.id) !== undefined;
|
|
260
260
|
}
|
|
@@ -277,7 +277,7 @@ let Activity = (() => {
|
|
|
277
277
|
canStart() {
|
|
278
278
|
if (!this.details?.route)
|
|
279
279
|
return false;
|
|
280
|
-
if (this.details.routeType === 'Free-Ride')
|
|
280
|
+
if (this.details.routeType === 'Free-Ride' || this.details.routeType === 'None')
|
|
281
281
|
return false;
|
|
282
282
|
if (this.details.routeType === 'Video') {
|
|
283
283
|
const isOnline = this.getOnlineMonitoring().onlineStatus;
|
|
@@ -198,7 +198,7 @@ let ActivityRideService = (() => {
|
|
|
198
198
|
this.disableDeviceHealthCheck();
|
|
199
199
|
this.state = 'completed';
|
|
200
200
|
this.updateActivityTime();
|
|
201
|
-
this._save();
|
|
201
|
+
await this._save();
|
|
202
202
|
this.emit('completed');
|
|
203
203
|
this.tsPauseStart = undefined;
|
|
204
204
|
delete this.tsStart;
|
|
@@ -536,7 +536,7 @@ let ActivityRideService = (() => {
|
|
|
536
536
|
if (this.state !== 'completed') {
|
|
537
537
|
await this.stop();
|
|
538
538
|
}
|
|
539
|
-
this.getRepo().delete(this.activity.id);
|
|
539
|
+
await this.getRepo().delete(this.activity.id);
|
|
540
540
|
this.state = 'idle';
|
|
541
541
|
}
|
|
542
542
|
save() {
|
|
@@ -552,7 +552,7 @@ let ActivityRideService = (() => {
|
|
|
552
552
|
let success = false;
|
|
553
553
|
try {
|
|
554
554
|
emit('start', success);
|
|
555
|
-
|
|
555
|
+
this._save();
|
|
556
556
|
success = await this._convertAndUpload();
|
|
557
557
|
(0, sleep_1.sleep)(0).then(() => {
|
|
558
558
|
delete this.saveObserver;
|
|
@@ -965,7 +965,7 @@ let ActivityRideService = (() => {
|
|
|
965
965
|
await fs.writeFile(fileName, Buffer.from(data));
|
|
966
966
|
this.activity.tcxFileName = fileName;
|
|
967
967
|
}
|
|
968
|
-
|
|
968
|
+
this._save();
|
|
969
969
|
emit('save.done', format, true);
|
|
970
970
|
return true;
|
|
971
971
|
}
|
|
@@ -22,8 +22,11 @@ function isVirtualShiftingEnabled(mode) {
|
|
|
22
22
|
function getLoadButtonMode(mode) {
|
|
23
23
|
if (!mode)
|
|
24
24
|
return 'power';
|
|
25
|
-
if (mode.
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
if (mode.isERG())
|
|
26
|
+
return 'power';
|
|
27
|
+
if (mode.isSIM() && isVirtualShiftingEnabled(mode))
|
|
28
|
+
return 'gear';
|
|
29
|
+
if (mode.isResistance())
|
|
30
|
+
return 'gear';
|
|
31
|
+
return 'hidden';
|
|
29
32
|
}
|
|
@@ -39,6 +39,7 @@ const gd_eventlog_1 = require("gd-eventlog");
|
|
|
39
39
|
const decorators_1 = require("../../base/decorators");
|
|
40
40
|
const pages_1 = require("../../base/pages");
|
|
41
41
|
const display_1 = require("../display");
|
|
42
|
+
const devices_1 = require("../../devices");
|
|
42
43
|
const sleep_1 = require("../../utils/sleep");
|
|
43
44
|
const monitoring_1 = require("../../monitoring");
|
|
44
45
|
const activities_1 = require("../../activities");
|
|
@@ -64,6 +65,7 @@ let RidePageService = (() => {
|
|
|
64
65
|
let _getActivityRide_decorators;
|
|
65
66
|
let _getUserSettings_decorators;
|
|
66
67
|
let _getOnlineStatusMonitoring_decorators;
|
|
68
|
+
let _getDeviceConfiguration_decorators;
|
|
67
69
|
var RidePageService = class extends _classSuper {
|
|
68
70
|
static { _classThis = this; }
|
|
69
71
|
static {
|
|
@@ -73,11 +75,13 @@ let RidePageService = (() => {
|
|
|
73
75
|
_getActivityRide_decorators = [decorators_1.Injectable];
|
|
74
76
|
_getUserSettings_decorators = [decorators_1.Injectable];
|
|
75
77
|
_getOnlineStatusMonitoring_decorators = [decorators_1.Injectable];
|
|
78
|
+
_getDeviceConfiguration_decorators = [decorators_1.Injectable];
|
|
76
79
|
__esDecorate(this, null, _getRideDisplay_decorators, { kind: "method", name: "getRideDisplay", static: false, private: false, access: { has: obj => "getRideDisplay" in obj, get: obj => obj.getRideDisplay }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
77
80
|
__esDecorate(this, null, _getWorkoutRide_decorators, { kind: "method", name: "getWorkoutRide", static: false, private: false, access: { has: obj => "getWorkoutRide" in obj, get: obj => obj.getWorkoutRide }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
78
81
|
__esDecorate(this, null, _getActivityRide_decorators, { kind: "method", name: "getActivityRide", static: false, private: false, access: { has: obj => "getActivityRide" in obj, get: obj => obj.getActivityRide }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
79
82
|
__esDecorate(this, null, _getUserSettings_decorators, { kind: "method", name: "getUserSettings", static: false, private: false, access: { has: obj => "getUserSettings" in obj, get: obj => obj.getUserSettings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
80
83
|
__esDecorate(this, null, _getOnlineStatusMonitoring_decorators, { kind: "method", name: "getOnlineStatusMonitoring", static: false, private: false, access: { has: obj => "getOnlineStatusMonitoring" in obj, get: obj => obj.getOnlineStatusMonitoring }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
84
|
+
__esDecorate(this, null, _getDeviceConfiguration_decorators, { kind: "method", name: "getDeviceConfiguration", static: false, private: false, access: { has: obj => "getDeviceConfiguration" in obj, get: obj => obj.getDeviceConfiguration }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
81
85
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
82
86
|
RidePageService = _classThis = _classDescriptor.value;
|
|
83
87
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
@@ -92,6 +96,7 @@ let RidePageService = (() => {
|
|
|
92
96
|
menuProps = null;
|
|
93
97
|
isInitialized = false;
|
|
94
98
|
startGateProps = null;
|
|
99
|
+
onDeviceModeChangedBound = this.onDeviceModeChanged.bind(this);
|
|
95
100
|
prevRidesVisibleRows = DEFAULT_PREV_RIDES_VISIBLE_ROWS;
|
|
96
101
|
prevRidesMode = 'list';
|
|
97
102
|
gestureHintDismissed = false;
|
|
@@ -103,7 +108,8 @@ let RidePageService = (() => {
|
|
|
103
108
|
this.eventHandler['route-update'] = this.onRouteUpdate.bind(this);
|
|
104
109
|
this.eventHandler['view-changed'] = this.onViewChanged.bind(this);
|
|
105
110
|
this.eventHandler['prev-rides-update'] = this.onPrevRidesTick.bind(this);
|
|
106
|
-
this.workoutEventHandler['step-changed'] = this.
|
|
111
|
+
this.workoutEventHandler['step-changed'] = this.onWorkoutStepChanged.bind(this);
|
|
112
|
+
this.workoutEventHandler['step-countdown'] = this.onWorkoutStepCountdown.bind(this);
|
|
107
113
|
this.workoutEventHandler['update'] = this.onWorkoutUpdate.bind(this);
|
|
108
114
|
this.workoutEventHandler['forward'] = this.onWorkoutUpdate.bind(this);
|
|
109
115
|
this.workoutEventHandler['backward'] = this.onWorkoutUpdate.bind(this);
|
|
@@ -142,6 +148,7 @@ let RidePageService = (() => {
|
|
|
142
148
|
const registerAndStart = () => {
|
|
143
149
|
this.registerHandlers(this.rideObserver, this.eventHandler);
|
|
144
150
|
this.subscribeToWorkoutObserver();
|
|
151
|
+
this.getDeviceConfiguration().on('mode-changed', this.onDeviceModeChangedBound);
|
|
145
152
|
const state = service.getState();
|
|
146
153
|
if (state === 'Started' || state === 'Active' || state === 'Paused') {
|
|
147
154
|
this.logError(new Error(`start() requested while ride is ${state}`), 'openPage');
|
|
@@ -186,6 +193,7 @@ let RidePageService = (() => {
|
|
|
186
193
|
this.getRideDisplay().stop();
|
|
187
194
|
this.unregisterHandlers(this.rideObserver, this.eventHandler);
|
|
188
195
|
this.unsubscribeFromWorkoutObserver();
|
|
196
|
+
this.getDeviceConfiguration().off('mode-changed', this.onDeviceModeChangedBound);
|
|
189
197
|
this.menuProps = null;
|
|
190
198
|
this.isInitialized = false;
|
|
191
199
|
super.closePage();
|
|
@@ -277,9 +285,11 @@ let RidePageService = (() => {
|
|
|
277
285
|
onMenuOpen() {
|
|
278
286
|
try {
|
|
279
287
|
const state = this.getRideDisplay().getState();
|
|
288
|
+
const loadControl = this.getLoadControlProps();
|
|
289
|
+
const showRideSettings = !this.isRideType('Workout');
|
|
280
290
|
this.menuProps = this.isWorkoutAttached()
|
|
281
|
-
? { showResume: state === 'Paused', ...this.getStepFlags() }
|
|
282
|
-
: { showResume: state === 'Paused' };
|
|
291
|
+
? { showResume: state === 'Paused', ...this.getStepFlags(), loadControl, showRideSettings }
|
|
292
|
+
: { showResume: state === 'Paused', loadControl, showRideSettings };
|
|
283
293
|
this.updatePageDisplay();
|
|
284
294
|
}
|
|
285
295
|
catch (err) {
|
|
@@ -396,7 +406,7 @@ let RidePageService = (() => {
|
|
|
396
406
|
const available = this.getCornerWidgetStates();
|
|
397
407
|
if (available.length <= 1)
|
|
398
408
|
return undefined;
|
|
399
|
-
const current = this.getUserSettings().get(CORNER_WIDGET_SETTING_KEY, '
|
|
409
|
+
const current = this.getUserSettings().get(CORNER_WIDGET_SETTING_KEY, 'workout');
|
|
400
410
|
return available.includes(current) ? current : available[0];
|
|
401
411
|
}
|
|
402
412
|
catch (err) {
|
|
@@ -413,7 +423,7 @@ let RidePageService = (() => {
|
|
|
413
423
|
onToggleCornerWidget() {
|
|
414
424
|
try {
|
|
415
425
|
const available = this.getCornerWidgetStates();
|
|
416
|
-
const current = this.getUserSettings().get(CORNER_WIDGET_SETTING_KEY, '
|
|
426
|
+
const current = this.getUserSettings().get(CORNER_WIDGET_SETTING_KEY, 'workout');
|
|
417
427
|
const next = available[(available.indexOf(current) + 1) % available.length];
|
|
418
428
|
this.getUserSettings().set(CORNER_WIDGET_SETTING_KEY, next);
|
|
419
429
|
this.updatePageDisplay();
|
|
@@ -582,8 +592,7 @@ let RidePageService = (() => {
|
|
|
582
592
|
const plainErgAdjustment = !this.getWorkoutRide().inUse() && this.getLoadButtonMode() === 'power';
|
|
583
593
|
if (plainErgAdjustment) {
|
|
584
594
|
const sign = deltaPct >= 0 ? 1 : -1;
|
|
585
|
-
|
|
586
|
-
return this.getRideDisplay().adjustDevicePower(sign * nominal);
|
|
595
|
+
return this.getRideDisplay().adjustDevicePower(sign * this.getNominalErgWatts(deltaPct));
|
|
587
596
|
}
|
|
588
597
|
return deltaPct >= 0
|
|
589
598
|
? this.getWorkoutRide().powerUp(deltaPct)
|
|
@@ -631,6 +640,13 @@ let RidePageService = (() => {
|
|
|
631
640
|
onWorkoutUpdate() {
|
|
632
641
|
this.updatePageDisplay();
|
|
633
642
|
}
|
|
643
|
+
onWorkoutStepChanged(update) {
|
|
644
|
+
this.onWorkoutUpdate();
|
|
645
|
+
this.getPageObserver()?.emit('step-changed', update);
|
|
646
|
+
}
|
|
647
|
+
onWorkoutStepCountdown(tick) {
|
|
648
|
+
this.getPageObserver()?.emit('step-countdown', tick);
|
|
649
|
+
}
|
|
634
650
|
onWorkoutFinished() {
|
|
635
651
|
try {
|
|
636
652
|
if (this.isRideType('Workout'))
|
|
@@ -756,6 +772,40 @@ let RidePageService = (() => {
|
|
|
756
772
|
const wo = this.getWorkoutRide().getDashboardDisplayProperties();
|
|
757
773
|
return { canStepBack: !!wo.canShowBackward, canStepForward: !!wo.canShowForward };
|
|
758
774
|
}
|
|
775
|
+
getLoadControlProps() {
|
|
776
|
+
const mode = this.getLoadButtonMode();
|
|
777
|
+
if (mode === 'hidden')
|
|
778
|
+
return { visible: false };
|
|
779
|
+
if (mode === 'gear')
|
|
780
|
+
return { visible: true, label: 'Gear', buttons: { inc1: '+1', dec1: '-1', inc5: '+5', dec5: '-5' } };
|
|
781
|
+
if (this.isWorkoutAttached()) {
|
|
782
|
+
const buttons = this.getWorkoutRide().getDashboardDisplayProperties().loadButtons;
|
|
783
|
+
return { visible: true, label: 'Load', buttons };
|
|
784
|
+
}
|
|
785
|
+
const small = this.getNominalErgWatts(1);
|
|
786
|
+
const big = this.getNominalErgWatts(5);
|
|
787
|
+
return {
|
|
788
|
+
visible: true,
|
|
789
|
+
label: 'Load',
|
|
790
|
+
buttons: { inc1: `+${small}W`, dec1: `-${small}W`, inc5: `+${big}W`, dec5: `-${big}W` }
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
getNominalErgWatts(magnitude) {
|
|
794
|
+
return Math.abs(magnitude) === 1 ? 5 : 50;
|
|
795
|
+
}
|
|
796
|
+
onDeviceModeChanged() {
|
|
797
|
+
setTimeout(() => {
|
|
798
|
+
try {
|
|
799
|
+
if (!this.menuProps)
|
|
800
|
+
return;
|
|
801
|
+
this.menuProps = { ...this.menuProps, loadControl: this.getLoadControlProps() };
|
|
802
|
+
this.updatePageDisplay();
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
this.logError(err, 'onDeviceModeChanged');
|
|
806
|
+
}
|
|
807
|
+
}, 0);
|
|
808
|
+
}
|
|
759
809
|
getLastLogTime() {
|
|
760
810
|
const logs = this.getActivityRide().getActivity()?.logs;
|
|
761
811
|
if (!logs || logs.length === 0)
|
|
@@ -812,14 +862,18 @@ let RidePageService = (() => {
|
|
|
812
862
|
}
|
|
813
863
|
}
|
|
814
864
|
buildPausedMenuProps() {
|
|
865
|
+
const loadControl = this.getLoadControlProps();
|
|
866
|
+
const showRideSettings = !this.isRideType('Workout');
|
|
815
867
|
return this.isWorkoutAttached()
|
|
816
|
-
? { showResume: true, ...this.getStepFlags() }
|
|
817
|
-
: { showResume: true };
|
|
868
|
+
? { showResume: true, ...this.getStepFlags(), loadControl, showRideSettings }
|
|
869
|
+
: { showResume: true, loadControl, showRideSettings };
|
|
818
870
|
}
|
|
819
871
|
buildFinishedMenuProps() {
|
|
872
|
+
const loadControl = this.getLoadControlProps();
|
|
873
|
+
const showRideSettings = !this.isRideType('Workout');
|
|
820
874
|
return this.isWorkoutAttached()
|
|
821
|
-
? { showResume: false, finished: true, canStepBack: false, canStepForward: false }
|
|
822
|
-
: { showResume: false, finished: true };
|
|
875
|
+
? { showResume: false, finished: true, canStepBack: false, canStepForward: false, loadControl, showRideSettings }
|
|
876
|
+
: { showResume: false, finished: true, loadControl, showRideSettings };
|
|
823
877
|
}
|
|
824
878
|
buildBaseDisplayProps() {
|
|
825
879
|
const state = this.getRideDisplay().getState();
|
|
@@ -953,6 +1007,9 @@ let RidePageService = (() => {
|
|
|
953
1007
|
getOnlineStatusMonitoring() {
|
|
954
1008
|
return (0, monitoring_1.useOnlineStatusMonitoring)();
|
|
955
1009
|
}
|
|
1010
|
+
getDeviceConfiguration() {
|
|
1011
|
+
return (0, devices_1.useDeviceConfiguration)();
|
|
1012
|
+
}
|
|
956
1013
|
};
|
|
957
1014
|
return RidePageService = _classThis;
|
|
958
1015
|
})();
|
|
@@ -609,6 +609,8 @@ let RouteListService = (() => {
|
|
|
609
609
|
}
|
|
610
610
|
getRouteDescription(id) {
|
|
611
611
|
try {
|
|
612
|
+
if (!id)
|
|
613
|
+
return undefined;
|
|
612
614
|
return this.getVisibleRoutes().find(r => r.description.id === id)?.description;
|
|
613
615
|
}
|
|
614
616
|
catch (err) {
|
|
@@ -617,6 +619,8 @@ let RouteListService = (() => {
|
|
|
617
619
|
}
|
|
618
620
|
getRoute(id) {
|
|
619
621
|
try {
|
|
622
|
+
if (!id)
|
|
623
|
+
return undefined;
|
|
620
624
|
return this.getVisibleRoutes().find(r => r.description.id === id);
|
|
621
625
|
}
|
|
622
626
|
catch (err) {
|
|
@@ -48,6 +48,10 @@ const list_1 = require("../list");
|
|
|
48
48
|
const decorators_1 = require("../../base/decorators");
|
|
49
49
|
const DEFAULT_FTP = 200;
|
|
50
50
|
const WORKOUT_ZOOM = 1200;
|
|
51
|
+
const STEP_COUNTDOWN_THRESHOLDS = [4, 3, 2, 1, 0];
|
|
52
|
+
const hasValidDuration = (duration) => {
|
|
53
|
+
return typeof duration === 'number' && Number.isFinite(duration) && duration > 0;
|
|
54
|
+
};
|
|
51
55
|
let WorkoutRide = (() => {
|
|
52
56
|
let _classDecorators = [types_1.Singleton];
|
|
53
57
|
let _classDescriptor;
|
|
@@ -86,6 +90,8 @@ let WorkoutRide = (() => {
|
|
|
86
90
|
tsCurrent;
|
|
87
91
|
trainingTime;
|
|
88
92
|
currentLimits;
|
|
93
|
+
countdownTimers = [];
|
|
94
|
+
firedCountdownThresholds = new Set();
|
|
89
95
|
updateInterval;
|
|
90
96
|
currentStep;
|
|
91
97
|
isFreeRide;
|
|
@@ -136,6 +142,8 @@ let WorkoutRide = (() => {
|
|
|
136
142
|
this.logEvent({ message: 'workout started', settings: this.settings });
|
|
137
143
|
if (!this.updateInterval) {
|
|
138
144
|
this.update();
|
|
145
|
+
this.resetCountdownProgress();
|
|
146
|
+
this.rescheduleStepCountdown();
|
|
139
147
|
this.updateInterval = setInterval(() => { this.update(); }, 500);
|
|
140
148
|
}
|
|
141
149
|
if (paused)
|
|
@@ -155,6 +163,7 @@ let WorkoutRide = (() => {
|
|
|
155
163
|
this.tsPauseStart = ts;
|
|
156
164
|
this.tsCurrent = ts;
|
|
157
165
|
this.state = 'paused';
|
|
166
|
+
this.clearCountdownTimers();
|
|
158
167
|
this.emit('paused');
|
|
159
168
|
this.logger.logEvent({ message: 'workout paused' });
|
|
160
169
|
}
|
|
@@ -178,6 +187,7 @@ let WorkoutRide = (() => {
|
|
|
178
187
|
this.tsPauseStart = undefined;
|
|
179
188
|
this.tsCurrent = ts;
|
|
180
189
|
this.state = 'active';
|
|
190
|
+
this.rescheduleStepCountdown();
|
|
181
191
|
this.logger.logEvent({ message: 'workout resumed', offset: this.offset });
|
|
182
192
|
this.emit('resumed');
|
|
183
193
|
}
|
|
@@ -219,6 +229,8 @@ let WorkoutRide = (() => {
|
|
|
219
229
|
}
|
|
220
230
|
this.manualTimeOffset += limits.remaining;
|
|
221
231
|
this.update();
|
|
232
|
+
this.resetCountdownProgress();
|
|
233
|
+
this.rescheduleStepCountdown();
|
|
222
234
|
this.observer.emit('forward', ts, limits.remaining);
|
|
223
235
|
}
|
|
224
236
|
catch (err) {
|
|
@@ -258,6 +270,8 @@ let WorkoutRide = (() => {
|
|
|
258
270
|
}
|
|
259
271
|
}
|
|
260
272
|
this.update();
|
|
273
|
+
this.resetCountdownProgress();
|
|
274
|
+
this.rescheduleStepCountdown();
|
|
261
275
|
this.observer.emit('backward', ts, diff, jumpType, limits?.step, target);
|
|
262
276
|
}
|
|
263
277
|
catch (err) {
|
|
@@ -508,7 +522,46 @@ let WorkoutRide = (() => {
|
|
|
508
522
|
else if (this.currentStep.power && !prevStep.power) {
|
|
509
523
|
this.stopFreeRide();
|
|
510
524
|
}
|
|
525
|
+
if (!this.firedCountdownThresholds.has(0)) {
|
|
526
|
+
this.emit('step-countdown', { secondsRemaining: 0 });
|
|
527
|
+
}
|
|
511
528
|
this.emit('step-changed', this.getDashboardDisplayProperties());
|
|
529
|
+
this.resetCountdownProgress();
|
|
530
|
+
this.rescheduleStepCountdown();
|
|
531
|
+
}
|
|
532
|
+
clearCountdownTimers() {
|
|
533
|
+
this.countdownTimers.forEach(t => clearTimeout(t));
|
|
534
|
+
this.countdownTimers = [];
|
|
535
|
+
}
|
|
536
|
+
resetCountdownProgress() {
|
|
537
|
+
this.firedCountdownThresholds = new Set();
|
|
538
|
+
}
|
|
539
|
+
rescheduleStepCountdown() {
|
|
540
|
+
this.clearCountdownTimers();
|
|
541
|
+
if (this.state !== 'active')
|
|
542
|
+
return;
|
|
543
|
+
const duration = this.currentLimits?.duration;
|
|
544
|
+
const remaining = this.currentLimits?.remaining;
|
|
545
|
+
const trainingTime = this.trainingTime;
|
|
546
|
+
if (!hasValidDuration(duration) || remaining === undefined || trainingTime === undefined)
|
|
547
|
+
return;
|
|
548
|
+
const stepEnd = trainingTime + remaining;
|
|
549
|
+
STEP_COUNTDOWN_THRESHOLDS.forEach(secondsRemaining => {
|
|
550
|
+
if (this.firedCountdownThresholds.has(secondsRemaining))
|
|
551
|
+
return;
|
|
552
|
+
const targetTrainingTime = stepEnd - secondsRemaining;
|
|
553
|
+
if (targetTrainingTime < trainingTime)
|
|
554
|
+
return;
|
|
555
|
+
const wallClockDeadline = this.tsStart + (this.offset ?? 0) + (targetTrainingTime - (this.manualTimeOffset ?? 0)) * 1000;
|
|
556
|
+
const delayMs = wallClockDeadline - Date.now();
|
|
557
|
+
if (delayMs < 0)
|
|
558
|
+
return;
|
|
559
|
+
const timer = setTimeout(() => {
|
|
560
|
+
this.firedCountdownThresholds.add(secondsRemaining);
|
|
561
|
+
this.emit('step-countdown', { secondsRemaining });
|
|
562
|
+
}, delayMs);
|
|
563
|
+
this.countdownTimers.push(timer);
|
|
564
|
+
});
|
|
512
565
|
}
|
|
513
566
|
checkIfDone() {
|
|
514
567
|
const time = (Date.now() - this.tsStart - (this.offset ?? 0)) / 1000 + (this.manualTimeOffset ?? 0);
|
|
@@ -691,6 +744,7 @@ let WorkoutRide = (() => {
|
|
|
691
744
|
clearInterval(this.updateInterval);
|
|
692
745
|
this.updateInterval = undefined;
|
|
693
746
|
}
|
|
747
|
+
this.clearCountdownTimers();
|
|
694
748
|
}
|
|
695
749
|
getCyclingModeText() {
|
|
696
750
|
const deviceRide = (0, devices_1.useDeviceRide)();
|
|
@@ -44,6 +44,7 @@ import { Injectable } from "../../../base/decorators/Injection";
|
|
|
44
44
|
import { useUnitConverter } from "../../../i18n";
|
|
45
45
|
export const DB_VERSION = '5';
|
|
46
46
|
export const DB_NAME = 'db';
|
|
47
|
+
const SHORT_RIDE_FILTER_CUTOFF = new Date('2026-08-28').getTime();
|
|
47
48
|
let ActivitiesRepository = (() => {
|
|
48
49
|
let _classDecorators = [Singleton];
|
|
49
50
|
let _classDescriptor;
|
|
@@ -106,7 +107,7 @@ let ActivitiesRepository = (() => {
|
|
|
106
107
|
this.write();
|
|
107
108
|
}
|
|
108
109
|
if (writeDetails) {
|
|
109
|
-
this.writeDetails(activity);
|
|
110
|
+
await this.writeDetails(activity);
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
migrate(activity, writeSummary = true) {
|
|
@@ -221,6 +222,16 @@ let ActivitiesRepository = (() => {
|
|
|
221
222
|
}
|
|
222
223
|
search(criteria) {
|
|
223
224
|
let result = this.activities;
|
|
225
|
+
if (!this.isMobile()) {
|
|
226
|
+
result = result?.filter(ai => {
|
|
227
|
+
const { rideTime, startTime } = ai.summary;
|
|
228
|
+
if (rideTime < 1)
|
|
229
|
+
return false;
|
|
230
|
+
if (rideTime <= 30 && startTime < SHORT_RIDE_FILTER_CUTOFF)
|
|
231
|
+
return false;
|
|
232
|
+
return true;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
224
235
|
result = this.checkIdFilter(result, criteria);
|
|
225
236
|
result = this.checkHashFilter(result, criteria);
|
|
226
237
|
result = this.checkStartPosFilter(result, criteria);
|
|
@@ -472,7 +483,7 @@ let ActivitiesRepository = (() => {
|
|
|
472
483
|
}
|
|
473
484
|
async writeDetails(activity) {
|
|
474
485
|
const name = activity.summary.name;
|
|
475
|
-
this.getRepo().write(name, activity.details);
|
|
486
|
+
await this.getRepo().write(name, activity.details);
|
|
476
487
|
}
|
|
477
488
|
async scanForNewActivities() {
|
|
478
489
|
try {
|
|
@@ -506,6 +517,13 @@ let ActivitiesRepository = (() => {
|
|
|
506
517
|
getRouteList() {
|
|
507
518
|
return useRouteList();
|
|
508
519
|
}
|
|
520
|
+
isMobile() {
|
|
521
|
+
const { appInfo } = this.getBindings();
|
|
522
|
+
if (!appInfo) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
return appInfo?.getChannel() === 'mobile';
|
|
526
|
+
}
|
|
509
527
|
getBindings() {
|
|
510
528
|
return getBindings();
|
|
511
529
|
}
|
|
@@ -251,7 +251,7 @@ let Activity = (() => {
|
|
|
251
251
|
isRouteAvailable() {
|
|
252
252
|
if (!this.details?.route)
|
|
253
253
|
return false;
|
|
254
|
-
if (this.details.routeType === 'Free-Ride')
|
|
254
|
+
if (this.details.routeType === 'Free-Ride' || this.details.routeType === 'None')
|
|
255
255
|
return false;
|
|
256
256
|
return this.getRouteList().getRouteDescription(this.details.route.id) !== undefined;
|
|
257
257
|
}
|
|
@@ -274,7 +274,7 @@ let Activity = (() => {
|
|
|
274
274
|
canStart() {
|
|
275
275
|
if (!this.details?.route)
|
|
276
276
|
return false;
|
|
277
|
-
if (this.details.routeType === 'Free-Ride')
|
|
277
|
+
if (this.details.routeType === 'Free-Ride' || this.details.routeType === 'None')
|
|
278
278
|
return false;
|
|
279
279
|
if (this.details.routeType === 'Video') {
|
|
280
280
|
const isOnline = this.getOnlineMonitoring().onlineStatus;
|
|
@@ -192,7 +192,7 @@ let ActivityRideService = (() => {
|
|
|
192
192
|
this.disableDeviceHealthCheck();
|
|
193
193
|
this.state = 'completed';
|
|
194
194
|
this.updateActivityTime();
|
|
195
|
-
this._save();
|
|
195
|
+
await this._save();
|
|
196
196
|
this.emit('completed');
|
|
197
197
|
this.tsPauseStart = undefined;
|
|
198
198
|
delete this.tsStart;
|
|
@@ -530,7 +530,7 @@ let ActivityRideService = (() => {
|
|
|
530
530
|
if (this.state !== 'completed') {
|
|
531
531
|
await this.stop();
|
|
532
532
|
}
|
|
533
|
-
this.getRepo().delete(this.activity.id);
|
|
533
|
+
await this.getRepo().delete(this.activity.id);
|
|
534
534
|
this.state = 'idle';
|
|
535
535
|
}
|
|
536
536
|
save() {
|
|
@@ -546,7 +546,7 @@ let ActivityRideService = (() => {
|
|
|
546
546
|
let success = false;
|
|
547
547
|
try {
|
|
548
548
|
emit('start', success);
|
|
549
|
-
|
|
549
|
+
this._save();
|
|
550
550
|
success = await this._convertAndUpload();
|
|
551
551
|
sleep(0).then(() => {
|
|
552
552
|
delete this.saveObserver;
|
|
@@ -959,7 +959,7 @@ let ActivityRideService = (() => {
|
|
|
959
959
|
await fs.writeFile(fileName, Buffer.from(data));
|
|
960
960
|
this.activity.tcxFileName = fileName;
|
|
961
961
|
}
|
|
962
|
-
|
|
962
|
+
this._save();
|
|
963
963
|
emit('save.done', format, true);
|
|
964
964
|
return true;
|
|
965
965
|
}
|
|
@@ -18,8 +18,11 @@ export function isVirtualShiftingEnabled(mode) {
|
|
|
18
18
|
export function getLoadButtonMode(mode) {
|
|
19
19
|
if (!mode)
|
|
20
20
|
return 'power';
|
|
21
|
-
if (mode.
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
if (mode.isERG())
|
|
22
|
+
return 'power';
|
|
23
|
+
if (mode.isSIM() && isVirtualShiftingEnabled(mode))
|
|
24
|
+
return 'gear';
|
|
25
|
+
if (mode.isResistance())
|
|
26
|
+
return 'gear';
|
|
27
|
+
return 'hidden';
|
|
25
28
|
}
|
|
@@ -36,6 +36,7 @@ import { EventLogger } from "gd-eventlog";
|
|
|
36
36
|
import { Injectable, Singleton } from "../../base/decorators";
|
|
37
37
|
import { IncyclistPageService } from "../../base/pages";
|
|
38
38
|
import { useRideDisplay } from "../display";
|
|
39
|
+
import { useDeviceConfiguration } from "../../devices";
|
|
39
40
|
import { sleep } from "../../utils/sleep";
|
|
40
41
|
import { useOnlineStatusMonitoring } from "../../monitoring";
|
|
41
42
|
import { useActivityRide } from "../../activities";
|
|
@@ -61,6 +62,7 @@ let RidePageService = (() => {
|
|
|
61
62
|
let _getActivityRide_decorators;
|
|
62
63
|
let _getUserSettings_decorators;
|
|
63
64
|
let _getOnlineStatusMonitoring_decorators;
|
|
65
|
+
let _getDeviceConfiguration_decorators;
|
|
64
66
|
var RidePageService = class extends _classSuper {
|
|
65
67
|
static { _classThis = this; }
|
|
66
68
|
static {
|
|
@@ -70,11 +72,13 @@ let RidePageService = (() => {
|
|
|
70
72
|
_getActivityRide_decorators = [Injectable];
|
|
71
73
|
_getUserSettings_decorators = [Injectable];
|
|
72
74
|
_getOnlineStatusMonitoring_decorators = [Injectable];
|
|
75
|
+
_getDeviceConfiguration_decorators = [Injectable];
|
|
73
76
|
__esDecorate(this, null, _getRideDisplay_decorators, { kind: "method", name: "getRideDisplay", static: false, private: false, access: { has: obj => "getRideDisplay" in obj, get: obj => obj.getRideDisplay }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
74
77
|
__esDecorate(this, null, _getWorkoutRide_decorators, { kind: "method", name: "getWorkoutRide", static: false, private: false, access: { has: obj => "getWorkoutRide" in obj, get: obj => obj.getWorkoutRide }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
75
78
|
__esDecorate(this, null, _getActivityRide_decorators, { kind: "method", name: "getActivityRide", static: false, private: false, access: { has: obj => "getActivityRide" in obj, get: obj => obj.getActivityRide }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
76
79
|
__esDecorate(this, null, _getUserSettings_decorators, { kind: "method", name: "getUserSettings", static: false, private: false, access: { has: obj => "getUserSettings" in obj, get: obj => obj.getUserSettings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
77
80
|
__esDecorate(this, null, _getOnlineStatusMonitoring_decorators, { kind: "method", name: "getOnlineStatusMonitoring", static: false, private: false, access: { has: obj => "getOnlineStatusMonitoring" in obj, get: obj => obj.getOnlineStatusMonitoring }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
81
|
+
__esDecorate(this, null, _getDeviceConfiguration_decorators, { kind: "method", name: "getDeviceConfiguration", static: false, private: false, access: { has: obj => "getDeviceConfiguration" in obj, get: obj => obj.getDeviceConfiguration }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
78
82
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
79
83
|
RidePageService = _classThis = _classDescriptor.value;
|
|
80
84
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
@@ -89,6 +93,7 @@ let RidePageService = (() => {
|
|
|
89
93
|
menuProps = null;
|
|
90
94
|
isInitialized = false;
|
|
91
95
|
startGateProps = null;
|
|
96
|
+
onDeviceModeChangedBound = this.onDeviceModeChanged.bind(this);
|
|
92
97
|
prevRidesVisibleRows = DEFAULT_PREV_RIDES_VISIBLE_ROWS;
|
|
93
98
|
prevRidesMode = 'list';
|
|
94
99
|
gestureHintDismissed = false;
|
|
@@ -100,7 +105,8 @@ let RidePageService = (() => {
|
|
|
100
105
|
this.eventHandler['route-update'] = this.onRouteUpdate.bind(this);
|
|
101
106
|
this.eventHandler['view-changed'] = this.onViewChanged.bind(this);
|
|
102
107
|
this.eventHandler['prev-rides-update'] = this.onPrevRidesTick.bind(this);
|
|
103
|
-
this.workoutEventHandler['step-changed'] = this.
|
|
108
|
+
this.workoutEventHandler['step-changed'] = this.onWorkoutStepChanged.bind(this);
|
|
109
|
+
this.workoutEventHandler['step-countdown'] = this.onWorkoutStepCountdown.bind(this);
|
|
104
110
|
this.workoutEventHandler['update'] = this.onWorkoutUpdate.bind(this);
|
|
105
111
|
this.workoutEventHandler['forward'] = this.onWorkoutUpdate.bind(this);
|
|
106
112
|
this.workoutEventHandler['backward'] = this.onWorkoutUpdate.bind(this);
|
|
@@ -139,6 +145,7 @@ let RidePageService = (() => {
|
|
|
139
145
|
const registerAndStart = () => {
|
|
140
146
|
this.registerHandlers(this.rideObserver, this.eventHandler);
|
|
141
147
|
this.subscribeToWorkoutObserver();
|
|
148
|
+
this.getDeviceConfiguration().on('mode-changed', this.onDeviceModeChangedBound);
|
|
142
149
|
const state = service.getState();
|
|
143
150
|
if (state === 'Started' || state === 'Active' || state === 'Paused') {
|
|
144
151
|
this.logError(new Error(`start() requested while ride is ${state}`), 'openPage');
|
|
@@ -183,6 +190,7 @@ let RidePageService = (() => {
|
|
|
183
190
|
this.getRideDisplay().stop();
|
|
184
191
|
this.unregisterHandlers(this.rideObserver, this.eventHandler);
|
|
185
192
|
this.unsubscribeFromWorkoutObserver();
|
|
193
|
+
this.getDeviceConfiguration().off('mode-changed', this.onDeviceModeChangedBound);
|
|
186
194
|
this.menuProps = null;
|
|
187
195
|
this.isInitialized = false;
|
|
188
196
|
super.closePage();
|
|
@@ -274,9 +282,11 @@ let RidePageService = (() => {
|
|
|
274
282
|
onMenuOpen() {
|
|
275
283
|
try {
|
|
276
284
|
const state = this.getRideDisplay().getState();
|
|
285
|
+
const loadControl = this.getLoadControlProps();
|
|
286
|
+
const showRideSettings = !this.isRideType('Workout');
|
|
277
287
|
this.menuProps = this.isWorkoutAttached()
|
|
278
|
-
? { showResume: state === 'Paused', ...this.getStepFlags() }
|
|
279
|
-
: { showResume: state === 'Paused' };
|
|
288
|
+
? { showResume: state === 'Paused', ...this.getStepFlags(), loadControl, showRideSettings }
|
|
289
|
+
: { showResume: state === 'Paused', loadControl, showRideSettings };
|
|
280
290
|
this.updatePageDisplay();
|
|
281
291
|
}
|
|
282
292
|
catch (err) {
|
|
@@ -393,7 +403,7 @@ let RidePageService = (() => {
|
|
|
393
403
|
const available = this.getCornerWidgetStates();
|
|
394
404
|
if (available.length <= 1)
|
|
395
405
|
return undefined;
|
|
396
|
-
const current = this.getUserSettings().get(CORNER_WIDGET_SETTING_KEY, '
|
|
406
|
+
const current = this.getUserSettings().get(CORNER_WIDGET_SETTING_KEY, 'workout');
|
|
397
407
|
return available.includes(current) ? current : available[0];
|
|
398
408
|
}
|
|
399
409
|
catch (err) {
|
|
@@ -410,7 +420,7 @@ let RidePageService = (() => {
|
|
|
410
420
|
onToggleCornerWidget() {
|
|
411
421
|
try {
|
|
412
422
|
const available = this.getCornerWidgetStates();
|
|
413
|
-
const current = this.getUserSettings().get(CORNER_WIDGET_SETTING_KEY, '
|
|
423
|
+
const current = this.getUserSettings().get(CORNER_WIDGET_SETTING_KEY, 'workout');
|
|
414
424
|
const next = available[(available.indexOf(current) + 1) % available.length];
|
|
415
425
|
this.getUserSettings().set(CORNER_WIDGET_SETTING_KEY, next);
|
|
416
426
|
this.updatePageDisplay();
|
|
@@ -579,8 +589,7 @@ let RidePageService = (() => {
|
|
|
579
589
|
const plainErgAdjustment = !this.getWorkoutRide().inUse() && this.getLoadButtonMode() === 'power';
|
|
580
590
|
if (plainErgAdjustment) {
|
|
581
591
|
const sign = deltaPct >= 0 ? 1 : -1;
|
|
582
|
-
|
|
583
|
-
return this.getRideDisplay().adjustDevicePower(sign * nominal);
|
|
592
|
+
return this.getRideDisplay().adjustDevicePower(sign * this.getNominalErgWatts(deltaPct));
|
|
584
593
|
}
|
|
585
594
|
return deltaPct >= 0
|
|
586
595
|
? this.getWorkoutRide().powerUp(deltaPct)
|
|
@@ -628,6 +637,13 @@ let RidePageService = (() => {
|
|
|
628
637
|
onWorkoutUpdate() {
|
|
629
638
|
this.updatePageDisplay();
|
|
630
639
|
}
|
|
640
|
+
onWorkoutStepChanged(update) {
|
|
641
|
+
this.onWorkoutUpdate();
|
|
642
|
+
this.getPageObserver()?.emit('step-changed', update);
|
|
643
|
+
}
|
|
644
|
+
onWorkoutStepCountdown(tick) {
|
|
645
|
+
this.getPageObserver()?.emit('step-countdown', tick);
|
|
646
|
+
}
|
|
631
647
|
onWorkoutFinished() {
|
|
632
648
|
try {
|
|
633
649
|
if (this.isRideType('Workout'))
|
|
@@ -753,6 +769,40 @@ let RidePageService = (() => {
|
|
|
753
769
|
const wo = this.getWorkoutRide().getDashboardDisplayProperties();
|
|
754
770
|
return { canStepBack: !!wo.canShowBackward, canStepForward: !!wo.canShowForward };
|
|
755
771
|
}
|
|
772
|
+
getLoadControlProps() {
|
|
773
|
+
const mode = this.getLoadButtonMode();
|
|
774
|
+
if (mode === 'hidden')
|
|
775
|
+
return { visible: false };
|
|
776
|
+
if (mode === 'gear')
|
|
777
|
+
return { visible: true, label: 'Gear', buttons: { inc1: '+1', dec1: '-1', inc5: '+5', dec5: '-5' } };
|
|
778
|
+
if (this.isWorkoutAttached()) {
|
|
779
|
+
const buttons = this.getWorkoutRide().getDashboardDisplayProperties().loadButtons;
|
|
780
|
+
return { visible: true, label: 'Load', buttons };
|
|
781
|
+
}
|
|
782
|
+
const small = this.getNominalErgWatts(1);
|
|
783
|
+
const big = this.getNominalErgWatts(5);
|
|
784
|
+
return {
|
|
785
|
+
visible: true,
|
|
786
|
+
label: 'Load',
|
|
787
|
+
buttons: { inc1: `+${small}W`, dec1: `-${small}W`, inc5: `+${big}W`, dec5: `-${big}W` }
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
getNominalErgWatts(magnitude) {
|
|
791
|
+
return Math.abs(magnitude) === 1 ? 5 : 50;
|
|
792
|
+
}
|
|
793
|
+
onDeviceModeChanged() {
|
|
794
|
+
setTimeout(() => {
|
|
795
|
+
try {
|
|
796
|
+
if (!this.menuProps)
|
|
797
|
+
return;
|
|
798
|
+
this.menuProps = { ...this.menuProps, loadControl: this.getLoadControlProps() };
|
|
799
|
+
this.updatePageDisplay();
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
this.logError(err, 'onDeviceModeChanged');
|
|
803
|
+
}
|
|
804
|
+
}, 0);
|
|
805
|
+
}
|
|
756
806
|
getLastLogTime() {
|
|
757
807
|
const logs = this.getActivityRide().getActivity()?.logs;
|
|
758
808
|
if (!logs || logs.length === 0)
|
|
@@ -809,14 +859,18 @@ let RidePageService = (() => {
|
|
|
809
859
|
}
|
|
810
860
|
}
|
|
811
861
|
buildPausedMenuProps() {
|
|
862
|
+
const loadControl = this.getLoadControlProps();
|
|
863
|
+
const showRideSettings = !this.isRideType('Workout');
|
|
812
864
|
return this.isWorkoutAttached()
|
|
813
|
-
? { showResume: true, ...this.getStepFlags() }
|
|
814
|
-
: { showResume: true };
|
|
865
|
+
? { showResume: true, ...this.getStepFlags(), loadControl, showRideSettings }
|
|
866
|
+
: { showResume: true, loadControl, showRideSettings };
|
|
815
867
|
}
|
|
816
868
|
buildFinishedMenuProps() {
|
|
869
|
+
const loadControl = this.getLoadControlProps();
|
|
870
|
+
const showRideSettings = !this.isRideType('Workout');
|
|
817
871
|
return this.isWorkoutAttached()
|
|
818
|
-
? { showResume: false, finished: true, canStepBack: false, canStepForward: false }
|
|
819
|
-
: { showResume: false, finished: true };
|
|
872
|
+
? { showResume: false, finished: true, canStepBack: false, canStepForward: false, loadControl, showRideSettings }
|
|
873
|
+
: { showResume: false, finished: true, loadControl, showRideSettings };
|
|
820
874
|
}
|
|
821
875
|
buildBaseDisplayProps() {
|
|
822
876
|
const state = this.getRideDisplay().getState();
|
|
@@ -950,6 +1004,9 @@ let RidePageService = (() => {
|
|
|
950
1004
|
getOnlineStatusMonitoring() {
|
|
951
1005
|
return useOnlineStatusMonitoring();
|
|
952
1006
|
}
|
|
1007
|
+
getDeviceConfiguration() {
|
|
1008
|
+
return useDeviceConfiguration();
|
|
1009
|
+
}
|
|
953
1010
|
};
|
|
954
1011
|
return RidePageService = _classThis;
|
|
955
1012
|
})();
|
|
@@ -603,6 +603,8 @@ let RouteListService = (() => {
|
|
|
603
603
|
}
|
|
604
604
|
getRouteDescription(id) {
|
|
605
605
|
try {
|
|
606
|
+
if (!id)
|
|
607
|
+
return undefined;
|
|
606
608
|
return this.getVisibleRoutes().find(r => r.description.id === id)?.description;
|
|
607
609
|
}
|
|
608
610
|
catch (err) {
|
|
@@ -611,6 +613,8 @@ let RouteListService = (() => {
|
|
|
611
613
|
}
|
|
612
614
|
getRoute(id) {
|
|
613
615
|
try {
|
|
616
|
+
if (!id)
|
|
617
|
+
return undefined;
|
|
614
618
|
return this.getVisibleRoutes().find(r => r.description.id === id);
|
|
615
619
|
}
|
|
616
620
|
catch (err) {
|
|
@@ -45,6 +45,10 @@ import { useWorkoutList } from "../list";
|
|
|
45
45
|
import { Injectable } from "../../base/decorators";
|
|
46
46
|
const DEFAULT_FTP = 200;
|
|
47
47
|
const WORKOUT_ZOOM = 1200;
|
|
48
|
+
const STEP_COUNTDOWN_THRESHOLDS = [4, 3, 2, 1, 0];
|
|
49
|
+
const hasValidDuration = (duration) => {
|
|
50
|
+
return typeof duration === 'number' && Number.isFinite(duration) && duration > 0;
|
|
51
|
+
};
|
|
48
52
|
let WorkoutRide = (() => {
|
|
49
53
|
let _classDecorators = [Singleton];
|
|
50
54
|
let _classDescriptor;
|
|
@@ -83,6 +87,8 @@ let WorkoutRide = (() => {
|
|
|
83
87
|
tsCurrent;
|
|
84
88
|
trainingTime;
|
|
85
89
|
currentLimits;
|
|
90
|
+
countdownTimers = [];
|
|
91
|
+
firedCountdownThresholds = new Set();
|
|
86
92
|
updateInterval;
|
|
87
93
|
currentStep;
|
|
88
94
|
isFreeRide;
|
|
@@ -133,6 +139,8 @@ let WorkoutRide = (() => {
|
|
|
133
139
|
this.logEvent({ message: 'workout started', settings: this.settings });
|
|
134
140
|
if (!this.updateInterval) {
|
|
135
141
|
this.update();
|
|
142
|
+
this.resetCountdownProgress();
|
|
143
|
+
this.rescheduleStepCountdown();
|
|
136
144
|
this.updateInterval = setInterval(() => { this.update(); }, 500);
|
|
137
145
|
}
|
|
138
146
|
if (paused)
|
|
@@ -152,6 +160,7 @@ let WorkoutRide = (() => {
|
|
|
152
160
|
this.tsPauseStart = ts;
|
|
153
161
|
this.tsCurrent = ts;
|
|
154
162
|
this.state = 'paused';
|
|
163
|
+
this.clearCountdownTimers();
|
|
155
164
|
this.emit('paused');
|
|
156
165
|
this.logger.logEvent({ message: 'workout paused' });
|
|
157
166
|
}
|
|
@@ -175,6 +184,7 @@ let WorkoutRide = (() => {
|
|
|
175
184
|
this.tsPauseStart = undefined;
|
|
176
185
|
this.tsCurrent = ts;
|
|
177
186
|
this.state = 'active';
|
|
187
|
+
this.rescheduleStepCountdown();
|
|
178
188
|
this.logger.logEvent({ message: 'workout resumed', offset: this.offset });
|
|
179
189
|
this.emit('resumed');
|
|
180
190
|
}
|
|
@@ -216,6 +226,8 @@ let WorkoutRide = (() => {
|
|
|
216
226
|
}
|
|
217
227
|
this.manualTimeOffset += limits.remaining;
|
|
218
228
|
this.update();
|
|
229
|
+
this.resetCountdownProgress();
|
|
230
|
+
this.rescheduleStepCountdown();
|
|
219
231
|
this.observer.emit('forward', ts, limits.remaining);
|
|
220
232
|
}
|
|
221
233
|
catch (err) {
|
|
@@ -255,6 +267,8 @@ let WorkoutRide = (() => {
|
|
|
255
267
|
}
|
|
256
268
|
}
|
|
257
269
|
this.update();
|
|
270
|
+
this.resetCountdownProgress();
|
|
271
|
+
this.rescheduleStepCountdown();
|
|
258
272
|
this.observer.emit('backward', ts, diff, jumpType, limits?.step, target);
|
|
259
273
|
}
|
|
260
274
|
catch (err) {
|
|
@@ -505,7 +519,46 @@ let WorkoutRide = (() => {
|
|
|
505
519
|
else if (this.currentStep.power && !prevStep.power) {
|
|
506
520
|
this.stopFreeRide();
|
|
507
521
|
}
|
|
522
|
+
if (!this.firedCountdownThresholds.has(0)) {
|
|
523
|
+
this.emit('step-countdown', { secondsRemaining: 0 });
|
|
524
|
+
}
|
|
508
525
|
this.emit('step-changed', this.getDashboardDisplayProperties());
|
|
526
|
+
this.resetCountdownProgress();
|
|
527
|
+
this.rescheduleStepCountdown();
|
|
528
|
+
}
|
|
529
|
+
clearCountdownTimers() {
|
|
530
|
+
this.countdownTimers.forEach(t => clearTimeout(t));
|
|
531
|
+
this.countdownTimers = [];
|
|
532
|
+
}
|
|
533
|
+
resetCountdownProgress() {
|
|
534
|
+
this.firedCountdownThresholds = new Set();
|
|
535
|
+
}
|
|
536
|
+
rescheduleStepCountdown() {
|
|
537
|
+
this.clearCountdownTimers();
|
|
538
|
+
if (this.state !== 'active')
|
|
539
|
+
return;
|
|
540
|
+
const duration = this.currentLimits?.duration;
|
|
541
|
+
const remaining = this.currentLimits?.remaining;
|
|
542
|
+
const trainingTime = this.trainingTime;
|
|
543
|
+
if (!hasValidDuration(duration) || remaining === undefined || trainingTime === undefined)
|
|
544
|
+
return;
|
|
545
|
+
const stepEnd = trainingTime + remaining;
|
|
546
|
+
STEP_COUNTDOWN_THRESHOLDS.forEach(secondsRemaining => {
|
|
547
|
+
if (this.firedCountdownThresholds.has(secondsRemaining))
|
|
548
|
+
return;
|
|
549
|
+
const targetTrainingTime = stepEnd - secondsRemaining;
|
|
550
|
+
if (targetTrainingTime < trainingTime)
|
|
551
|
+
return;
|
|
552
|
+
const wallClockDeadline = this.tsStart + (this.offset ?? 0) + (targetTrainingTime - (this.manualTimeOffset ?? 0)) * 1000;
|
|
553
|
+
const delayMs = wallClockDeadline - Date.now();
|
|
554
|
+
if (delayMs < 0)
|
|
555
|
+
return;
|
|
556
|
+
const timer = setTimeout(() => {
|
|
557
|
+
this.firedCountdownThresholds.add(secondsRemaining);
|
|
558
|
+
this.emit('step-countdown', { secondsRemaining });
|
|
559
|
+
}, delayMs);
|
|
560
|
+
this.countdownTimers.push(timer);
|
|
561
|
+
});
|
|
509
562
|
}
|
|
510
563
|
checkIfDone() {
|
|
511
564
|
const time = (Date.now() - this.tsStart - (this.offset ?? 0)) / 1000 + (this.manualTimeOffset ?? 0);
|
|
@@ -688,6 +741,7 @@ let WorkoutRide = (() => {
|
|
|
688
741
|
clearInterval(this.updateInterval);
|
|
689
742
|
this.updateInterval = undefined;
|
|
690
743
|
}
|
|
744
|
+
this.clearCountdownTimers();
|
|
691
745
|
}
|
|
692
746
|
getCyclingModeText() {
|
|
693
747
|
const deviceRide = useDeviceRide();
|
|
@@ -55,6 +55,7 @@ export declare class ActivitiesRepository {
|
|
|
55
55
|
protected logError(err: Error, fn: string, logProps?: any): void;
|
|
56
56
|
protected getRepo(): JsonRepository;
|
|
57
57
|
protected getRouteList(): import("../../../routes").RouteListService;
|
|
58
|
+
protected isMobile(): boolean;
|
|
58
59
|
protected getBindings(): import("../../../api").IncyclistBindings;
|
|
59
60
|
protected getUnitConverter(): import("../../../i18n").UnitConverterService;
|
|
60
61
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { IncyclistPageService } from "../../base/pages";
|
|
2
2
|
import { CurrentRideDisplayProps, CurrentRideState, IObserver, RideType } from "../../types";
|
|
3
|
-
import { AnyRidePageDisplayProps, GPXRidePageDisplayProps, IRidePageService, PrevRidesRowProps, RideMenuProps, RidePageDisplayProps, StartGateProps, VideoRidePageDisplayProps, WorkoutDashboardLine, WorkoutGestureHint, WorkoutGraphActuals, WorkoutGraphPlan, WorkoutRideMenuProps, WorkoutRidePageDisplayProps, WorkoutUpcomingSteps } from "./types";
|
|
3
|
+
import { AnyRidePageDisplayProps, GPXRidePageDisplayProps, IRidePageService, PrevRidesRowProps, LoadControlButtonLabels, RideMenuProps, RidePageDisplayProps, StartGateProps, VideoRidePageDisplayProps, WorkoutDashboardLine, WorkoutGestureHint, WorkoutGraphActuals, WorkoutGraphPlan, WorkoutRideMenuProps, WorkoutRidePageDisplayProps, WorkoutUpcomingSteps } from "./types";
|
|
4
4
|
import { ISecretBinding } from "../../api/secret";
|
|
5
5
|
import { PrevRidesListDisplayProps } from "../../activities";
|
|
6
|
-
import type { LoadButtonMode, PowerAdjustmentResult, WorkoutDisplayProperties } from "../../workouts/ride/types";
|
|
6
|
+
import type { LoadButtonMode, PowerAdjustmentResult, StepCountdownTick, WorkoutDisplayProperties } from "../../workouts/ride/types";
|
|
7
7
|
import type { Workout } from "../../workouts/base/model";
|
|
8
8
|
import type { StepDefinition } from "../../workouts/base/model/types";
|
|
9
9
|
export declare class RidePageService extends IncyclistPageService implements IRidePageService {
|
|
@@ -16,6 +16,7 @@ export declare class RidePageService extends IncyclistPageService implements IRi
|
|
|
16
16
|
protected menuProps: RideMenuProps | null;
|
|
17
17
|
protected isInitialized: boolean;
|
|
18
18
|
protected startGateProps: StartGateProps | null;
|
|
19
|
+
protected onDeviceModeChangedBound: any;
|
|
19
20
|
protected prevRidesVisibleRows: number;
|
|
20
21
|
protected prevRidesMode: 'condensed' | 'list';
|
|
21
22
|
protected gestureHintDismissed: boolean;
|
|
@@ -74,6 +75,8 @@ export declare class RidePageService extends IncyclistPageService implements IRi
|
|
|
74
75
|
getLoadButtonMode(): LoadButtonMode;
|
|
75
76
|
protected getWorkoutRideDisplayProps(): WorkoutRidePageDisplayProps;
|
|
76
77
|
protected onWorkoutUpdate(): void;
|
|
78
|
+
protected onWorkoutStepChanged(update?: WorkoutDisplayProperties): void;
|
|
79
|
+
protected onWorkoutStepCountdown(tick?: StepCountdownTick): void;
|
|
77
80
|
protected onWorkoutFinished(): void;
|
|
78
81
|
protected finishRide(): void;
|
|
79
82
|
protected subscribeToWorkoutObserver(): void;
|
|
@@ -89,6 +92,13 @@ export declare class RidePageService extends IncyclistPageService implements IRi
|
|
|
89
92
|
canStepBack: boolean;
|
|
90
93
|
canStepForward: boolean;
|
|
91
94
|
};
|
|
95
|
+
protected getLoadControlProps(): {
|
|
96
|
+
visible: boolean;
|
|
97
|
+
label?: 'Load' | 'Gear';
|
|
98
|
+
buttons?: LoadControlButtonLabels;
|
|
99
|
+
};
|
|
100
|
+
protected getNominalErgWatts(magnitude: number): number;
|
|
101
|
+
protected onDeviceModeChanged(): void;
|
|
92
102
|
protected getLastLogTime(): number;
|
|
93
103
|
protected getElapsedActivityTime(): number;
|
|
94
104
|
protected getEmptyWorkoutDisplayProps(): WorkoutRidePageDisplayProps;
|
|
@@ -129,5 +139,6 @@ export declare class RidePageService extends IncyclistPageService implements IRi
|
|
|
129
139
|
protected getActivityRide(): import("../../activities").ActivityRideService;
|
|
130
140
|
protected getUserSettings(): import("../../settings").UserSettingsService;
|
|
131
141
|
protected getOnlineStatusMonitoring(): import("../../monitoring").OnlineStateMonitoringService;
|
|
142
|
+
protected getDeviceConfiguration(): import("../../devices").DeviceConfigurationService;
|
|
132
143
|
}
|
|
133
144
|
export declare const getRidePageService: () => IRidePageService;
|
|
@@ -9,11 +9,23 @@ export interface StartGateProps {
|
|
|
9
9
|
title: string;
|
|
10
10
|
body: string;
|
|
11
11
|
}
|
|
12
|
+
export interface LoadControlButtonLabels {
|
|
13
|
+
inc1: string;
|
|
14
|
+
dec1: string;
|
|
15
|
+
inc5: string;
|
|
16
|
+
dec5: string;
|
|
17
|
+
}
|
|
12
18
|
export interface RideMenuProps {
|
|
13
19
|
showResume?: boolean;
|
|
14
20
|
finished?: boolean;
|
|
15
21
|
canStepBack?: boolean;
|
|
16
22
|
canStepForward?: boolean;
|
|
23
|
+
loadControl?: {
|
|
24
|
+
visible: boolean;
|
|
25
|
+
label?: 'Load' | 'Gear';
|
|
26
|
+
buttons?: LoadControlButtonLabels;
|
|
27
|
+
};
|
|
28
|
+
showRideSettings?: boolean;
|
|
17
29
|
}
|
|
18
30
|
export interface RidePageDisplayProps {
|
|
19
31
|
rideState: CurrentRideState;
|
|
@@ -139,6 +151,12 @@ export interface WorkoutDashboardLine {
|
|
|
139
151
|
export type WorkoutRideMenuProps = RideMenuProps & {
|
|
140
152
|
canStepBack: boolean;
|
|
141
153
|
canStepForward: boolean;
|
|
154
|
+
loadControl: {
|
|
155
|
+
visible: boolean;
|
|
156
|
+
label?: 'Load' | 'Gear';
|
|
157
|
+
buttons?: LoadControlButtonLabels;
|
|
158
|
+
};
|
|
159
|
+
showRideSettings: boolean;
|
|
142
160
|
};
|
|
143
161
|
export interface WorkoutGestureHint {
|
|
144
162
|
visible: boolean;
|
|
@@ -20,6 +20,8 @@ export declare class WorkoutRide extends IncyclistService {
|
|
|
20
20
|
protected tsCurrent: number;
|
|
21
21
|
protected trainingTime: number;
|
|
22
22
|
protected currentLimits: ActiveWorkoutLimit;
|
|
23
|
+
private countdownTimers;
|
|
24
|
+
private firedCountdownThresholds;
|
|
23
25
|
protected updateInterval: NodeJS.Timeout;
|
|
24
26
|
protected currentStep: StepDefinition;
|
|
25
27
|
protected isFreeRide: boolean;
|
|
@@ -56,6 +58,9 @@ export declare class WorkoutRide extends IncyclistService {
|
|
|
56
58
|
getObserver(): Observer;
|
|
57
59
|
protected update(startIfInitialized?: boolean): void;
|
|
58
60
|
private onStepChange;
|
|
61
|
+
private clearCountdownTimers;
|
|
62
|
+
private resetCountdownProgress;
|
|
63
|
+
private rescheduleStepCountdown;
|
|
59
64
|
private checkIfDone;
|
|
60
65
|
protected startFreeRide(): Promise<void>;
|
|
61
66
|
protected stopFreeRide(): Promise<void>;
|