incyclist-services 1.7.88 → 1.7.90
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/ride/service.js +35 -3
- package/lib/cjs/appstate/index.js +1 -0
- package/lib/cjs/appstate/toggle-sync.js +174 -0
- package/lib/cjs/devices/configuration/service.js +2 -2
- package/lib/cjs/routes/list/loaders/types.js +1 -1
- package/lib/cjs/routes/list/service.js +2 -1
- package/lib/cjs/ui/service.js +11 -3
- package/lib/cjs/workouts/page/service.js +8 -1
- package/lib/cjs/workouts/ride/page/service.js +15 -6
- package/lib/esm/activities/ride/service.js +35 -3
- package/lib/esm/appstate/index.js +1 -0
- package/lib/esm/appstate/toggle-sync.js +170 -0
- package/lib/esm/devices/configuration/service.js +2 -2
- package/lib/esm/routes/list/loaders/types.js +1 -1
- package/lib/esm/routes/list/service.js +2 -1
- package/lib/esm/ui/service.js +12 -4
- package/lib/esm/workouts/page/service.js +8 -1
- package/lib/esm/workouts/ride/page/service.js +15 -6
- package/lib/types/activities/ride/service.d.ts +2 -1
- package/lib/types/activities/ride/types.d.ts +7 -0
- package/lib/types/appstate/index.d.ts +1 -0
- package/lib/types/appstate/toggle-sync.d.ts +22 -0
- package/lib/types/ui/service.d.ts +1 -0
- package/lib/types/workouts/ride/page/service.d.ts +2 -1
- package/lib/types/workouts/ride/page/types.d.ts +2 -3
- package/package.json +2 -2
|
@@ -48,6 +48,7 @@ const incyclist_devices_1 = require("incyclist-devices");
|
|
|
48
48
|
const devices_1 = require("../../devices");
|
|
49
49
|
const base_1 = require("../base");
|
|
50
50
|
const uuid_1 = require("uuid");
|
|
51
|
+
const workouts_1 = require("../../workouts");
|
|
51
52
|
const route_1 = require("../../routes/base/utils/route");
|
|
52
53
|
const route_2 = require("../../routes/base/model/route");
|
|
53
54
|
const stats_1 = require("./stats");
|
|
@@ -425,14 +426,15 @@ let ActivityRideService = (() => {
|
|
|
425
426
|
const route = this.getRouteList().getSelected();
|
|
426
427
|
const startSettings = this.getRouteList().getStartSettings();
|
|
427
428
|
const isFreeRide = startSettings?.type === 'Free-Ride';
|
|
428
|
-
const
|
|
429
|
+
const showWorkoutSummary = this.activity?.routeType === 'None';
|
|
429
430
|
const showSave = this.activity !== undefined && !this.isSaveDone;
|
|
430
431
|
const showContinue = this.state !== 'completed';
|
|
431
432
|
const hasGPX = this.activity?.logs?.some((log) => !!log.lat && !!log.lng);
|
|
432
|
-
const showMap = hasGPX || isFreeRide ||
|
|
433
|
+
const showMap = hasGPX || isFreeRide || (route?.description?.hasGpx);
|
|
433
434
|
const preview = showMap ? undefined : route?.description?.previewUrl;
|
|
435
|
+
const workoutGraph = showWorkoutSummary ? this.buildWorkoutSummaryGraph(this.activity) : undefined;
|
|
434
436
|
if (!this.isSummaryShown) {
|
|
435
|
-
this.logEvent({ message: 'activity summary shown', showSave, showContinue, showMap, fileLink: this.activity?.fitFileName });
|
|
437
|
+
this.logEvent({ message: 'activity summary shown', showSave, showContinue, showMap, showWorkoutSummary, fileLink: this.activity?.fitFileName });
|
|
436
438
|
}
|
|
437
439
|
this.isSummaryShown = true;
|
|
438
440
|
const props = {
|
|
@@ -440,6 +442,8 @@ let ActivityRideService = (() => {
|
|
|
440
442
|
showSave,
|
|
441
443
|
showContinue,
|
|
442
444
|
showMap,
|
|
445
|
+
showWorkoutSummary,
|
|
446
|
+
workoutGraph,
|
|
443
447
|
showDonate: this.canShowDonate(),
|
|
444
448
|
preview,
|
|
445
449
|
units: this.getUnitConverter().getDefaultUnits()
|
|
@@ -451,6 +455,34 @@ let ActivityRideService = (() => {
|
|
|
451
455
|
return {};
|
|
452
456
|
}
|
|
453
457
|
}
|
|
458
|
+
buildWorkoutSummaryGraph(activity) {
|
|
459
|
+
const workout = activity?.workout;
|
|
460
|
+
if (!workout)
|
|
461
|
+
return undefined;
|
|
462
|
+
const ftp = activity.user?.ftp ?? 0;
|
|
463
|
+
const bars = (0, workouts_1.getWorkoutGraphSeries)(workout, { ftp, absValues: true });
|
|
464
|
+
const lastBarX = bars.length ? bars.at(-1).x : 0;
|
|
465
|
+
const lastLogTime = activity.logs?.length ? activity.logs.at(-1).time : 0;
|
|
466
|
+
const maxXCandidates = [lastLogTime, lastBarX, workout.duration ?? 0].filter(Number.isFinite);
|
|
467
|
+
const maxX = maxXCandidates.length ? Math.max(...maxXCandidates) : 0;
|
|
468
|
+
const maxBarPower = bars.length ? Math.max(...bars.map(b => b.y)) : 0;
|
|
469
|
+
const plan = {
|
|
470
|
+
bars,
|
|
471
|
+
ftp,
|
|
472
|
+
ftpLine: ftp,
|
|
473
|
+
domain: { x: [0, maxX], y: [0, maxBarPower * 1.1] }
|
|
474
|
+
};
|
|
475
|
+
const actuals = {
|
|
476
|
+
power: (activity.logs ?? [])
|
|
477
|
+
.filter(log => Number.isFinite(log.time) && Number.isFinite(log.power))
|
|
478
|
+
.map(log => ({ x: log.time, y: log.power })),
|
|
479
|
+
heartrate: (activity.logs ?? [])
|
|
480
|
+
.filter(log => Number.isFinite(log.time) && Number.isFinite(log.heartrate))
|
|
481
|
+
.map(log => ({ x: log.time, y: log.heartrate })),
|
|
482
|
+
position: -1
|
|
483
|
+
};
|
|
484
|
+
return { plan, actuals };
|
|
485
|
+
}
|
|
454
486
|
canShowDonate() {
|
|
455
487
|
try {
|
|
456
488
|
if (this.isDonateShown) {
|
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./service"), exports);
|
|
18
18
|
__exportStar(require("./types"), exports);
|
|
19
|
+
__exportStar(require("./toggle-sync"), exports);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
3
|
+
var useValue = arguments.length > 2;
|
|
4
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
5
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
6
|
+
}
|
|
7
|
+
return useValue ? value : void 0;
|
|
8
|
+
};
|
|
9
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
10
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
11
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
12
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
13
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
14
|
+
var _, done = false;
|
|
15
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
16
|
+
var context = {};
|
|
17
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
18
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
19
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
20
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
21
|
+
if (kind === "accessor") {
|
|
22
|
+
if (result === void 0) continue;
|
|
23
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
24
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
25
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
26
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
27
|
+
}
|
|
28
|
+
else if (_ = accept(result)) {
|
|
29
|
+
if (kind === "field") initializers.unshift(_);
|
|
30
|
+
else descriptor[key] = _;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
34
|
+
done = true;
|
|
35
|
+
};
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.useFeatureToggleSync = exports.FeatureToggleSyncService = void 0;
|
|
38
|
+
const api_1 = require("../api");
|
|
39
|
+
const decorators_1 = require("../base/decorators");
|
|
40
|
+
const service_1 = require("../base/service");
|
|
41
|
+
const singleton_1 = require("../base/types/singleton");
|
|
42
|
+
const settings_1 = require("../settings");
|
|
43
|
+
const TOPIC_PREFIX = 'incyclist/features';
|
|
44
|
+
let FeatureToggleSyncService = (() => {
|
|
45
|
+
let _classDecorators = [singleton_1.Singleton];
|
|
46
|
+
let _classDescriptor;
|
|
47
|
+
let _classExtraInitializers = [];
|
|
48
|
+
let _classThis;
|
|
49
|
+
let _classSuper = service_1.IncyclistService;
|
|
50
|
+
let _instanceExtraInitializers = [];
|
|
51
|
+
let _getMessageQueue_decorators;
|
|
52
|
+
let _getUserSettings_decorators;
|
|
53
|
+
var FeatureToggleSyncService = class extends _classSuper {
|
|
54
|
+
static { _classThis = this; }
|
|
55
|
+
static {
|
|
56
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
57
|
+
_getMessageQueue_decorators = [decorators_1.Injectable];
|
|
58
|
+
_getUserSettings_decorators = [decorators_1.Injectable];
|
|
59
|
+
__esDecorate(this, null, _getMessageQueue_decorators, { kind: "method", name: "getMessageQueue", static: false, private: false, access: { has: obj => "getMessageQueue" in obj, get: obj => obj.getMessageQueue }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
60
|
+
__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);
|
|
61
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
62
|
+
FeatureToggleSyncService = _classThis = _classDescriptor.value;
|
|
63
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
64
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
65
|
+
}
|
|
66
|
+
uuid = __runInitializers(this, _instanceExtraInitializers);
|
|
67
|
+
subscribed = false;
|
|
68
|
+
onMessageHandler = this.onMessage.bind(this);
|
|
69
|
+
constructor() {
|
|
70
|
+
super('FeatureToggleSync');
|
|
71
|
+
}
|
|
72
|
+
start(uuid) {
|
|
73
|
+
try {
|
|
74
|
+
if (!uuid)
|
|
75
|
+
return;
|
|
76
|
+
if (this.subscribed && this.uuid === uuid)
|
|
77
|
+
return;
|
|
78
|
+
if (this.subscribed)
|
|
79
|
+
this.stop();
|
|
80
|
+
const mq = this.getMessageQueue();
|
|
81
|
+
if (!mq?.enabled?.()) {
|
|
82
|
+
this.logEvent({ message: 'feature-toggle sync not started', reason: 'mq binding not enabled', uuid });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.uuid = uuid;
|
|
86
|
+
mq.on('mq-message', this.onMessageHandler);
|
|
87
|
+
mq.subscribe(this.getTopic(uuid));
|
|
88
|
+
this.subscribed = true;
|
|
89
|
+
this.logEvent({ message: 'feature-toggle sync started', uuid, topic: this.getTopic(uuid) });
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
this.logError(err, 'start');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
stop() {
|
|
96
|
+
try {
|
|
97
|
+
if (!this.subscribed)
|
|
98
|
+
return;
|
|
99
|
+
const mq = this.getMessageQueue();
|
|
100
|
+
if (mq) {
|
|
101
|
+
mq.off('mq-message', this.onMessageHandler);
|
|
102
|
+
if (this.uuid)
|
|
103
|
+
mq.unsubscribe(this.getTopic(this.uuid));
|
|
104
|
+
}
|
|
105
|
+
this.logEvent({ message: 'feature-toggle sync stopped', uuid: this.uuid });
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
this.logError(err, 'stop');
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
this.subscribed = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
getTopic(uuid) {
|
|
115
|
+
return `${TOPIC_PREFIX}/${uuid}/+`;
|
|
116
|
+
}
|
|
117
|
+
getTopicPrefix() {
|
|
118
|
+
return this.uuid ? `${TOPIC_PREFIX}/${this.uuid}/` : undefined;
|
|
119
|
+
}
|
|
120
|
+
onMessage(topic, message) {
|
|
121
|
+
try {
|
|
122
|
+
const prefix = this.getTopicPrefix();
|
|
123
|
+
if (!prefix || typeof topic !== 'string' || !topic.startsWith(prefix))
|
|
124
|
+
return;
|
|
125
|
+
const toggleName = this.parseToggleName(topic, prefix);
|
|
126
|
+
if (!toggleName) {
|
|
127
|
+
this.logEvent({ message: 'feature-toggle sync: ignoring malformed topic', topic, payload: message });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const payload = this.parsePayload(message);
|
|
131
|
+
if (!payload || typeof payload.value !== 'boolean') {
|
|
132
|
+
this.logEvent({ message: 'feature-toggle sync: ignoring malformed payload', topic, payload: message });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.getUserSettings().set(toggleName, payload.value);
|
|
136
|
+
this.logEvent({ message: 'feature-toggle sync: applied toggle', toggle: toggleName, value: payload.value });
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
this.logError(err, 'onMessage', { topic });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
parseToggleName(topic, prefix) {
|
|
143
|
+
const toggleName = topic.slice(prefix.length);
|
|
144
|
+
if (!toggleName || toggleName.includes('/'))
|
|
145
|
+
return undefined;
|
|
146
|
+
return toggleName;
|
|
147
|
+
}
|
|
148
|
+
parsePayload(message) {
|
|
149
|
+
try {
|
|
150
|
+
let str;
|
|
151
|
+
if (typeof message === 'string')
|
|
152
|
+
str = message;
|
|
153
|
+
else if (message instanceof Uint8Array)
|
|
154
|
+
str = Buffer.from(message.buffer).toString();
|
|
155
|
+
else
|
|
156
|
+
return undefined;
|
|
157
|
+
return JSON.parse(str);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
getMessageQueue() {
|
|
164
|
+
return (0, api_1.getBindings)().mq;
|
|
165
|
+
}
|
|
166
|
+
getUserSettings() {
|
|
167
|
+
return (0, settings_1.useUserSettings)();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
return FeatureToggleSyncService = _classThis;
|
|
171
|
+
})();
|
|
172
|
+
exports.FeatureToggleSyncService = FeatureToggleSyncService;
|
|
173
|
+
const useFeatureToggleSync = () => new FeatureToggleSyncService();
|
|
174
|
+
exports.useFeatureToggleSync = useFeatureToggleSync;
|
|
@@ -222,8 +222,8 @@ let DeviceConfigurationService = (() => {
|
|
|
222
222
|
if (c.capability === 'bike')
|
|
223
223
|
return;
|
|
224
224
|
const capability = c.capability;
|
|
225
|
-
if (!capabilties.includes(capability)
|
|
226
|
-
this.delete(udid, capability);
|
|
225
|
+
if (!capabilties.includes(capability)) {
|
|
226
|
+
this.delete(udid, capability, true);
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
this.updateUserSettings();
|
|
@@ -8,7 +8,7 @@ const utils_2 = require("../../base/parsers/utils");
|
|
|
8
8
|
const route_1 = require("../../base/utils/route");
|
|
9
9
|
const MALFORMED_VIDEO_URL = /^video:\/{4,}/;
|
|
10
10
|
const correctVideoUrl = (url, isMobile = false) => {
|
|
11
|
-
if (!url
|
|
11
|
+
if (!url?.startsWith('video:'))
|
|
12
12
|
return undefined;
|
|
13
13
|
const malformed = MALFORMED_VIDEO_URL.test(url);
|
|
14
14
|
if (!malformed && !isMobile)
|
|
@@ -888,13 +888,14 @@ let RouteListService = (() => {
|
|
|
888
888
|
if (!route.description.country) {
|
|
889
889
|
route.updateCountryFromPoints()
|
|
890
890
|
.then(() => {
|
|
891
|
-
this.logEvent({ message: 'preload route updated
|
|
891
|
+
this.logEvent({ message: 'preload route updated', route: card?.getData()?.title, reason: 'country added' });
|
|
892
892
|
this.db.save(route, false);
|
|
893
893
|
});
|
|
894
894
|
}
|
|
895
895
|
if (route.description.hasVideo && route.description.isDownloaded) {
|
|
896
896
|
card.videoExists().then(exists => {
|
|
897
897
|
if (!exists) {
|
|
898
|
+
this.logEvent({ message: 'preload route updated', route: card?.getData()?.title, reason: 'downloaded file missing' });
|
|
898
899
|
card.resetDownload();
|
|
899
900
|
}
|
|
900
901
|
});
|
package/lib/cjs/ui/service.js
CHANGED
|
@@ -63,6 +63,7 @@ let UserInterfaceServcie = (() => {
|
|
|
63
63
|
let _getUserSettings_decorators;
|
|
64
64
|
let _getOnlineStatusMonitoring_decorators;
|
|
65
65
|
let _getAppState_decorators;
|
|
66
|
+
let _getFeatureToggleSync_decorators;
|
|
66
67
|
var UserInterfaceServcie = class extends _classSuper {
|
|
67
68
|
static { _classThis = this; }
|
|
68
69
|
static {
|
|
@@ -72,11 +73,13 @@ let UserInterfaceServcie = (() => {
|
|
|
72
73
|
_getUserSettings_decorators = [decorators_1.Injectable];
|
|
73
74
|
_getOnlineStatusMonitoring_decorators = [decorators_1.Injectable];
|
|
74
75
|
_getAppState_decorators = [decorators_1.Injectable];
|
|
76
|
+
_getFeatureToggleSync_decorators = [decorators_1.Injectable];
|
|
75
77
|
__esDecorate(this, null, _getBindings_decorators, { kind: "method", name: "getBindings", static: false, private: false, access: { has: obj => "getBindings" in obj, get: obj => obj.getBindings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
76
78
|
__esDecorate(this, null, _getMessageQueue_decorators, { kind: "method", name: "getMessageQueue", static: false, private: false, access: { has: obj => "getMessageQueue" in obj, get: obj => obj.getMessageQueue }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
77
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);
|
|
78
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);
|
|
79
81
|
__esDecorate(this, null, _getAppState_decorators, { kind: "method", name: "getAppState", static: false, private: false, access: { has: obj => "getAppState" in obj, get: obj => obj.getAppState }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
82
|
+
__esDecorate(this, null, _getFeatureToggleSync_decorators, { kind: "method", name: "getFeatureToggleSync", static: false, private: false, access: { has: obj => "getFeatureToggleSync" in obj, get: obj => obj.getFeatureToggleSync }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
80
83
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
81
84
|
UserInterfaceServcie = _classThis = _classDescriptor.value;
|
|
82
85
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
@@ -205,9 +208,10 @@ let UserInterfaceServcie = (() => {
|
|
|
205
208
|
try {
|
|
206
209
|
this.startQueueWorker();
|
|
207
210
|
this.startHeartbeatWorker();
|
|
208
|
-
|
|
211
|
+
let sent = false;
|
|
209
212
|
const user = this.getUserSettings().getValue('user', {});
|
|
210
213
|
const id = this.getUserSettings().getValue('uuid', undefined);
|
|
214
|
+
this.getFeatureToggleSync().start(id);
|
|
211
215
|
const { username, weight, ftp, gender } = user;
|
|
212
216
|
const topic = `incyclist/session/${this.session}/start`;
|
|
213
217
|
const os = this.getBindings().appInfo?.getOS()?.platform;
|
|
@@ -219,7 +223,7 @@ let UserInterfaceServcie = (() => {
|
|
|
219
223
|
started: new Date().toISOString()
|
|
220
224
|
};
|
|
221
225
|
if (this.isMobile() || this.isOnline()) {
|
|
222
|
-
this.sendMessage(topic, payload);
|
|
226
|
+
sent = this.sendMessage(topic, payload);
|
|
223
227
|
}
|
|
224
228
|
if (!sent) {
|
|
225
229
|
this.queueMessage(topic, payload);
|
|
@@ -360,7 +364,8 @@ let UserInterfaceServcie = (() => {
|
|
|
360
364
|
version: this.version,
|
|
361
365
|
appVersion: appInfo.getAppVersion(),
|
|
362
366
|
uuid: this.getUserSettings().get('uuid', undefined),
|
|
363
|
-
session: this.session
|
|
367
|
+
session: this.session,
|
|
368
|
+
'app-channel': this.platform
|
|
364
369
|
};
|
|
365
370
|
this.logger.logEvent({ message: 'setting globals', ...globals });
|
|
366
371
|
this.logger.setGlobal(globals);
|
|
@@ -500,6 +505,9 @@ let UserInterfaceServcie = (() => {
|
|
|
500
505
|
getAppState() {
|
|
501
506
|
return (0, appstate_1.useAppState)();
|
|
502
507
|
}
|
|
508
|
+
getFeatureToggleSync() {
|
|
509
|
+
return (0, appstate_1.useFeatureToggleSync)();
|
|
510
|
+
}
|
|
503
511
|
};
|
|
504
512
|
return UserInterfaceServcie = _classThis;
|
|
505
513
|
})();
|
|
@@ -93,7 +93,11 @@ let WorkoutListPageService = (() => {
|
|
|
93
93
|
openPage() {
|
|
94
94
|
try {
|
|
95
95
|
super.openPage();
|
|
96
|
-
|
|
96
|
+
let pageType;
|
|
97
|
+
if (!this.getAppState().hasFeature('MOBILE_WORKOUTS')) {
|
|
98
|
+
pageType = 'placeholder';
|
|
99
|
+
}
|
|
100
|
+
this.logEvent({ message: 'page shown', page: 'Workouts', pageType });
|
|
97
101
|
const { observer } = this.getWorkoutList().open();
|
|
98
102
|
this.listObserver = observer;
|
|
99
103
|
this.listObserver?.on('started', this.listUpdateHandler);
|
|
@@ -323,10 +327,12 @@ let WorkoutListPageService = (() => {
|
|
|
323
327
|
this.importPhase = 'importing';
|
|
324
328
|
this.importingFileName = file?.name;
|
|
325
329
|
this.emitImportUpdate();
|
|
330
|
+
this.logEvent({ message: 'workout import attempted', file: file?.name });
|
|
326
331
|
this.getWorkoutList().import(file, { showImportCards: false })
|
|
327
332
|
.then(([card]) => {
|
|
328
333
|
const group = this.getLastUsedImportGroup();
|
|
329
334
|
observer.emit('success');
|
|
335
|
+
this.logEvent({ message: 'workout import succeeded', file: file?.name, workoutName: card.getTitle(), group });
|
|
330
336
|
if (generation !== this.importGeneration)
|
|
331
337
|
return;
|
|
332
338
|
if (group !== DEFAULT_IMPORT_GROUP)
|
|
@@ -337,6 +343,7 @@ let WorkoutListPageService = (() => {
|
|
|
337
343
|
})
|
|
338
344
|
.catch((err) => {
|
|
339
345
|
observer.emit('error', err);
|
|
346
|
+
this.logEvent({ message: 'workout import failed', file: file?.name, error: err?.message });
|
|
340
347
|
if (generation !== this.importGeneration)
|
|
341
348
|
return;
|
|
342
349
|
this.importPhase = 'error';
|
|
@@ -91,8 +91,8 @@ let WorkoutRidePageService = (() => {
|
|
|
91
91
|
this.workoutEventHandler['update'] = this.onWorkoutUpdate.bind(this);
|
|
92
92
|
this.workoutEventHandler['forward'] = this.onWorkoutUpdate.bind(this);
|
|
93
93
|
this.workoutEventHandler['backward'] = this.onWorkoutUpdate.bind(this);
|
|
94
|
-
this.workoutEventHandler['completed'] = this.
|
|
95
|
-
this.workoutEventHandler['stopped'] = this.
|
|
94
|
+
this.workoutEventHandler['completed'] = this.onFinished.bind(this);
|
|
95
|
+
this.workoutEventHandler['stopped'] = this.onFinished.bind(this);
|
|
96
96
|
}
|
|
97
97
|
openPage(simulate) {
|
|
98
98
|
try {
|
|
@@ -119,7 +119,7 @@ let WorkoutRidePageService = (() => {
|
|
|
119
119
|
try {
|
|
120
120
|
gd_eventlog_1.EventLogger.setGlobalConfig('page', null);
|
|
121
121
|
this.logEvent({ message: 'page closed', page: 'WorkoutRide' });
|
|
122
|
-
this.getRideDisplay().stop(
|
|
122
|
+
this.getRideDisplay().stop();
|
|
123
123
|
this.unregisterRideEventHandlers();
|
|
124
124
|
this.unsubscribeFromWorkoutObserver();
|
|
125
125
|
this.menuProps = null;
|
|
@@ -243,8 +243,7 @@ let WorkoutRidePageService = (() => {
|
|
|
243
243
|
}
|
|
244
244
|
onStop() {
|
|
245
245
|
try {
|
|
246
|
-
this.
|
|
247
|
-
this.emitNavigateBack();
|
|
246
|
+
this.finishRide();
|
|
248
247
|
}
|
|
249
248
|
catch (err) {
|
|
250
249
|
this.logError(err, 'onStop');
|
|
@@ -353,7 +352,17 @@ let WorkoutRidePageService = (() => {
|
|
|
353
352
|
onWorkoutUpdate() {
|
|
354
353
|
this.updatePageDisplay();
|
|
355
354
|
}
|
|
356
|
-
|
|
355
|
+
onFinished() {
|
|
356
|
+
try {
|
|
357
|
+
this.menuProps = { showResume: false, finished: true, canStepBack: false, canStepForward: false };
|
|
358
|
+
this.updatePageDisplay();
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
this.logError(err, 'onFinished');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
finishRide() {
|
|
365
|
+
this.getRideDisplay().stop(true);
|
|
357
366
|
this.emitNavigateBack();
|
|
358
367
|
}
|
|
359
368
|
subscribeToWorkoutObserver() {
|
|
@@ -42,6 +42,7 @@ import { IncyclistCapability } from "incyclist-devices";
|
|
|
42
42
|
import { useDeviceConfiguration, useDeviceRide } from "../../devices";
|
|
43
43
|
import { ActivitiesRepository, ActivityConverter, ActivityConverterFactory, DB_VERSION, DEFAULT_SPORT_ACTIVITY_TITLE } from "../base";
|
|
44
44
|
import { v4 as generateUUID } from 'uuid';
|
|
45
|
+
import { getWorkoutGraphSeries } from "../../workouts";
|
|
45
46
|
import { addDetails, checkIsLoop, getElevationGainAt, getNextPosition, getPosition, getRouteHash, validateRoute } from "../../routes/base/utils/route";
|
|
46
47
|
import { Route } from "../../routes/base/model/route";
|
|
47
48
|
import { ActivityStatsCalculator } from "./stats";
|
|
@@ -419,14 +420,15 @@ let ActivityRideService = (() => {
|
|
|
419
420
|
const route = this.getRouteList().getSelected();
|
|
420
421
|
const startSettings = this.getRouteList().getStartSettings();
|
|
421
422
|
const isFreeRide = startSettings?.type === 'Free-Ride';
|
|
422
|
-
const
|
|
423
|
+
const showWorkoutSummary = this.activity?.routeType === 'None';
|
|
423
424
|
const showSave = this.activity !== undefined && !this.isSaveDone;
|
|
424
425
|
const showContinue = this.state !== 'completed';
|
|
425
426
|
const hasGPX = this.activity?.logs?.some((log) => !!log.lat && !!log.lng);
|
|
426
|
-
const showMap = hasGPX || isFreeRide ||
|
|
427
|
+
const showMap = hasGPX || isFreeRide || (route?.description?.hasGpx);
|
|
427
428
|
const preview = showMap ? undefined : route?.description?.previewUrl;
|
|
429
|
+
const workoutGraph = showWorkoutSummary ? this.buildWorkoutSummaryGraph(this.activity) : undefined;
|
|
428
430
|
if (!this.isSummaryShown) {
|
|
429
|
-
this.logEvent({ message: 'activity summary shown', showSave, showContinue, showMap, fileLink: this.activity?.fitFileName });
|
|
431
|
+
this.logEvent({ message: 'activity summary shown', showSave, showContinue, showMap, showWorkoutSummary, fileLink: this.activity?.fitFileName });
|
|
430
432
|
}
|
|
431
433
|
this.isSummaryShown = true;
|
|
432
434
|
const props = {
|
|
@@ -434,6 +436,8 @@ let ActivityRideService = (() => {
|
|
|
434
436
|
showSave,
|
|
435
437
|
showContinue,
|
|
436
438
|
showMap,
|
|
439
|
+
showWorkoutSummary,
|
|
440
|
+
workoutGraph,
|
|
437
441
|
showDonate: this.canShowDonate(),
|
|
438
442
|
preview,
|
|
439
443
|
units: this.getUnitConverter().getDefaultUnits()
|
|
@@ -445,6 +449,34 @@ let ActivityRideService = (() => {
|
|
|
445
449
|
return {};
|
|
446
450
|
}
|
|
447
451
|
}
|
|
452
|
+
buildWorkoutSummaryGraph(activity) {
|
|
453
|
+
const workout = activity?.workout;
|
|
454
|
+
if (!workout)
|
|
455
|
+
return undefined;
|
|
456
|
+
const ftp = activity.user?.ftp ?? 0;
|
|
457
|
+
const bars = getWorkoutGraphSeries(workout, { ftp, absValues: true });
|
|
458
|
+
const lastBarX = bars.length ? bars.at(-1).x : 0;
|
|
459
|
+
const lastLogTime = activity.logs?.length ? activity.logs.at(-1).time : 0;
|
|
460
|
+
const maxXCandidates = [lastLogTime, lastBarX, workout.duration ?? 0].filter(Number.isFinite);
|
|
461
|
+
const maxX = maxXCandidates.length ? Math.max(...maxXCandidates) : 0;
|
|
462
|
+
const maxBarPower = bars.length ? Math.max(...bars.map(b => b.y)) : 0;
|
|
463
|
+
const plan = {
|
|
464
|
+
bars,
|
|
465
|
+
ftp,
|
|
466
|
+
ftpLine: ftp,
|
|
467
|
+
domain: { x: [0, maxX], y: [0, maxBarPower * 1.1] }
|
|
468
|
+
};
|
|
469
|
+
const actuals = {
|
|
470
|
+
power: (activity.logs ?? [])
|
|
471
|
+
.filter(log => Number.isFinite(log.time) && Number.isFinite(log.power))
|
|
472
|
+
.map(log => ({ x: log.time, y: log.power })),
|
|
473
|
+
heartrate: (activity.logs ?? [])
|
|
474
|
+
.filter(log => Number.isFinite(log.time) && Number.isFinite(log.heartrate))
|
|
475
|
+
.map(log => ({ x: log.time, y: log.heartrate })),
|
|
476
|
+
position: -1
|
|
477
|
+
};
|
|
478
|
+
return { plan, actuals };
|
|
479
|
+
}
|
|
448
480
|
canShowDonate() {
|
|
449
481
|
try {
|
|
450
482
|
if (this.isDonateShown) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import { getBindings } from "../api";
|
|
36
|
+
import { Injectable } from "../base/decorators";
|
|
37
|
+
import { IncyclistService } from "../base/service";
|
|
38
|
+
import { Singleton } from "../base/types/singleton";
|
|
39
|
+
import { useUserSettings } from "../settings";
|
|
40
|
+
const TOPIC_PREFIX = 'incyclist/features';
|
|
41
|
+
let FeatureToggleSyncService = (() => {
|
|
42
|
+
let _classDecorators = [Singleton];
|
|
43
|
+
let _classDescriptor;
|
|
44
|
+
let _classExtraInitializers = [];
|
|
45
|
+
let _classThis;
|
|
46
|
+
let _classSuper = IncyclistService;
|
|
47
|
+
let _instanceExtraInitializers = [];
|
|
48
|
+
let _getMessageQueue_decorators;
|
|
49
|
+
let _getUserSettings_decorators;
|
|
50
|
+
var FeatureToggleSyncService = class extends _classSuper {
|
|
51
|
+
static { _classThis = this; }
|
|
52
|
+
static {
|
|
53
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
54
|
+
_getMessageQueue_decorators = [Injectable];
|
|
55
|
+
_getUserSettings_decorators = [Injectable];
|
|
56
|
+
__esDecorate(this, null, _getMessageQueue_decorators, { kind: "method", name: "getMessageQueue", static: false, private: false, access: { has: obj => "getMessageQueue" in obj, get: obj => obj.getMessageQueue }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
57
|
+
__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);
|
|
58
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
59
|
+
FeatureToggleSyncService = _classThis = _classDescriptor.value;
|
|
60
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
61
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
62
|
+
}
|
|
63
|
+
uuid = __runInitializers(this, _instanceExtraInitializers);
|
|
64
|
+
subscribed = false;
|
|
65
|
+
onMessageHandler = this.onMessage.bind(this);
|
|
66
|
+
constructor() {
|
|
67
|
+
super('FeatureToggleSync');
|
|
68
|
+
}
|
|
69
|
+
start(uuid) {
|
|
70
|
+
try {
|
|
71
|
+
if (!uuid)
|
|
72
|
+
return;
|
|
73
|
+
if (this.subscribed && this.uuid === uuid)
|
|
74
|
+
return;
|
|
75
|
+
if (this.subscribed)
|
|
76
|
+
this.stop();
|
|
77
|
+
const mq = this.getMessageQueue();
|
|
78
|
+
if (!mq?.enabled?.()) {
|
|
79
|
+
this.logEvent({ message: 'feature-toggle sync not started', reason: 'mq binding not enabled', uuid });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.uuid = uuid;
|
|
83
|
+
mq.on('mq-message', this.onMessageHandler);
|
|
84
|
+
mq.subscribe(this.getTopic(uuid));
|
|
85
|
+
this.subscribed = true;
|
|
86
|
+
this.logEvent({ message: 'feature-toggle sync started', uuid, topic: this.getTopic(uuid) });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
this.logError(err, 'start');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
stop() {
|
|
93
|
+
try {
|
|
94
|
+
if (!this.subscribed)
|
|
95
|
+
return;
|
|
96
|
+
const mq = this.getMessageQueue();
|
|
97
|
+
if (mq) {
|
|
98
|
+
mq.off('mq-message', this.onMessageHandler);
|
|
99
|
+
if (this.uuid)
|
|
100
|
+
mq.unsubscribe(this.getTopic(this.uuid));
|
|
101
|
+
}
|
|
102
|
+
this.logEvent({ message: 'feature-toggle sync stopped', uuid: this.uuid });
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
this.logError(err, 'stop');
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
this.subscribed = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
getTopic(uuid) {
|
|
112
|
+
return `${TOPIC_PREFIX}/${uuid}/+`;
|
|
113
|
+
}
|
|
114
|
+
getTopicPrefix() {
|
|
115
|
+
return this.uuid ? `${TOPIC_PREFIX}/${this.uuid}/` : undefined;
|
|
116
|
+
}
|
|
117
|
+
onMessage(topic, message) {
|
|
118
|
+
try {
|
|
119
|
+
const prefix = this.getTopicPrefix();
|
|
120
|
+
if (!prefix || typeof topic !== 'string' || !topic.startsWith(prefix))
|
|
121
|
+
return;
|
|
122
|
+
const toggleName = this.parseToggleName(topic, prefix);
|
|
123
|
+
if (!toggleName) {
|
|
124
|
+
this.logEvent({ message: 'feature-toggle sync: ignoring malformed topic', topic, payload: message });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const payload = this.parsePayload(message);
|
|
128
|
+
if (!payload || typeof payload.value !== 'boolean') {
|
|
129
|
+
this.logEvent({ message: 'feature-toggle sync: ignoring malformed payload', topic, payload: message });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.getUserSettings().set(toggleName, payload.value);
|
|
133
|
+
this.logEvent({ message: 'feature-toggle sync: applied toggle', toggle: toggleName, value: payload.value });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
this.logError(err, 'onMessage', { topic });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
parseToggleName(topic, prefix) {
|
|
140
|
+
const toggleName = topic.slice(prefix.length);
|
|
141
|
+
if (!toggleName || toggleName.includes('/'))
|
|
142
|
+
return undefined;
|
|
143
|
+
return toggleName;
|
|
144
|
+
}
|
|
145
|
+
parsePayload(message) {
|
|
146
|
+
try {
|
|
147
|
+
let str;
|
|
148
|
+
if (typeof message === 'string')
|
|
149
|
+
str = message;
|
|
150
|
+
else if (message instanceof Uint8Array)
|
|
151
|
+
str = Buffer.from(message.buffer).toString();
|
|
152
|
+
else
|
|
153
|
+
return undefined;
|
|
154
|
+
return JSON.parse(str);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
getMessageQueue() {
|
|
161
|
+
return getBindings().mq;
|
|
162
|
+
}
|
|
163
|
+
getUserSettings() {
|
|
164
|
+
return useUserSettings();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
return FeatureToggleSyncService = _classThis;
|
|
168
|
+
})();
|
|
169
|
+
export { FeatureToggleSyncService };
|
|
170
|
+
export const useFeatureToggleSync = () => new FeatureToggleSyncService();
|
|
@@ -216,8 +216,8 @@ let DeviceConfigurationService = (() => {
|
|
|
216
216
|
if (c.capability === 'bike')
|
|
217
217
|
return;
|
|
218
218
|
const capability = c.capability;
|
|
219
|
-
if (!capabilties.includes(capability)
|
|
220
|
-
this.delete(udid, capability);
|
|
219
|
+
if (!capabilties.includes(capability)) {
|
|
220
|
+
this.delete(udid, capability, true);
|
|
221
221
|
}
|
|
222
222
|
});
|
|
223
223
|
this.updateUserSettings();
|
|
@@ -5,7 +5,7 @@ import { buildFileUrl, buildVideoUrl } from "../../base/parsers/utils";
|
|
|
5
5
|
import { getRouteHash } from "../../base/utils/route";
|
|
6
6
|
const MALFORMED_VIDEO_URL = /^video:\/{4,}/;
|
|
7
7
|
export const correctVideoUrl = (url, isMobile = false) => {
|
|
8
|
-
if (!url
|
|
8
|
+
if (!url?.startsWith('video:'))
|
|
9
9
|
return undefined;
|
|
10
10
|
const malformed = MALFORMED_VIDEO_URL.test(url);
|
|
11
11
|
if (!malformed && !isMobile)
|
|
@@ -882,13 +882,14 @@ let RouteListService = (() => {
|
|
|
882
882
|
if (!route.description.country) {
|
|
883
883
|
route.updateCountryFromPoints()
|
|
884
884
|
.then(() => {
|
|
885
|
-
this.logEvent({ message: 'preload route updated
|
|
885
|
+
this.logEvent({ message: 'preload route updated', route: card?.getData()?.title, reason: 'country added' });
|
|
886
886
|
this.db.save(route, false);
|
|
887
887
|
});
|
|
888
888
|
}
|
|
889
889
|
if (route.description.hasVideo && route.description.isDownloaded) {
|
|
890
890
|
card.videoExists().then(exists => {
|
|
891
891
|
if (!exists) {
|
|
892
|
+
this.logEvent({ message: 'preload route updated', route: card?.getData()?.title, reason: 'downloaded file missing' });
|
|
892
893
|
card.resetDownload();
|
|
893
894
|
}
|
|
894
895
|
});
|
package/lib/esm/ui/service.js
CHANGED
|
@@ -44,7 +44,7 @@ import { useFreeRideService, useRouteList } from "../routes";
|
|
|
44
44
|
import { useWorkoutList } from "../workouts";
|
|
45
45
|
import { useActiveRides, useActivityList } from "../activities";
|
|
46
46
|
import { useOnlineStatusMonitoring } from "../monitoring";
|
|
47
|
-
import { useAppState } from "../appstate";
|
|
47
|
+
import { useAppState, useFeatureToggleSync } from "../appstate";
|
|
48
48
|
import { IncyclistPageService } from "../base/pages";
|
|
49
49
|
import { waitNextTick } from "../utils";
|
|
50
50
|
const BACKGROUND_PAUSE_TIMEOUT_MS = 300000;
|
|
@@ -60,6 +60,7 @@ let UserInterfaceServcie = (() => {
|
|
|
60
60
|
let _getUserSettings_decorators;
|
|
61
61
|
let _getOnlineStatusMonitoring_decorators;
|
|
62
62
|
let _getAppState_decorators;
|
|
63
|
+
let _getFeatureToggleSync_decorators;
|
|
63
64
|
var UserInterfaceServcie = class extends _classSuper {
|
|
64
65
|
static { _classThis = this; }
|
|
65
66
|
static {
|
|
@@ -69,11 +70,13 @@ let UserInterfaceServcie = (() => {
|
|
|
69
70
|
_getUserSettings_decorators = [Injectable];
|
|
70
71
|
_getOnlineStatusMonitoring_decorators = [Injectable];
|
|
71
72
|
_getAppState_decorators = [Injectable];
|
|
73
|
+
_getFeatureToggleSync_decorators = [Injectable];
|
|
72
74
|
__esDecorate(this, null, _getBindings_decorators, { kind: "method", name: "getBindings", static: false, private: false, access: { has: obj => "getBindings" in obj, get: obj => obj.getBindings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
73
75
|
__esDecorate(this, null, _getMessageQueue_decorators, { kind: "method", name: "getMessageQueue", static: false, private: false, access: { has: obj => "getMessageQueue" in obj, get: obj => obj.getMessageQueue }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
74
76
|
__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);
|
|
75
77
|
__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);
|
|
76
78
|
__esDecorate(this, null, _getAppState_decorators, { kind: "method", name: "getAppState", static: false, private: false, access: { has: obj => "getAppState" in obj, get: obj => obj.getAppState }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
79
|
+
__esDecorate(this, null, _getFeatureToggleSync_decorators, { kind: "method", name: "getFeatureToggleSync", static: false, private: false, access: { has: obj => "getFeatureToggleSync" in obj, get: obj => obj.getFeatureToggleSync }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
77
80
|
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
78
81
|
UserInterfaceServcie = _classThis = _classDescriptor.value;
|
|
79
82
|
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
@@ -202,9 +205,10 @@ let UserInterfaceServcie = (() => {
|
|
|
202
205
|
try {
|
|
203
206
|
this.startQueueWorker();
|
|
204
207
|
this.startHeartbeatWorker();
|
|
205
|
-
|
|
208
|
+
let sent = false;
|
|
206
209
|
const user = this.getUserSettings().getValue('user', {});
|
|
207
210
|
const id = this.getUserSettings().getValue('uuid', undefined);
|
|
211
|
+
this.getFeatureToggleSync().start(id);
|
|
208
212
|
const { username, weight, ftp, gender } = user;
|
|
209
213
|
const topic = `incyclist/session/${this.session}/start`;
|
|
210
214
|
const os = this.getBindings().appInfo?.getOS()?.platform;
|
|
@@ -216,7 +220,7 @@ let UserInterfaceServcie = (() => {
|
|
|
216
220
|
started: new Date().toISOString()
|
|
217
221
|
};
|
|
218
222
|
if (this.isMobile() || this.isOnline()) {
|
|
219
|
-
this.sendMessage(topic, payload);
|
|
223
|
+
sent = this.sendMessage(topic, payload);
|
|
220
224
|
}
|
|
221
225
|
if (!sent) {
|
|
222
226
|
this.queueMessage(topic, payload);
|
|
@@ -357,7 +361,8 @@ let UserInterfaceServcie = (() => {
|
|
|
357
361
|
version: this.version,
|
|
358
362
|
appVersion: appInfo.getAppVersion(),
|
|
359
363
|
uuid: this.getUserSettings().get('uuid', undefined),
|
|
360
|
-
session: this.session
|
|
364
|
+
session: this.session,
|
|
365
|
+
'app-channel': this.platform
|
|
361
366
|
};
|
|
362
367
|
this.logger.logEvent({ message: 'setting globals', ...globals });
|
|
363
368
|
this.logger.setGlobal(globals);
|
|
@@ -497,6 +502,9 @@ let UserInterfaceServcie = (() => {
|
|
|
497
502
|
getAppState() {
|
|
498
503
|
return useAppState();
|
|
499
504
|
}
|
|
505
|
+
getFeatureToggleSync() {
|
|
506
|
+
return useFeatureToggleSync();
|
|
507
|
+
}
|
|
500
508
|
};
|
|
501
509
|
return UserInterfaceServcie = _classThis;
|
|
502
510
|
})();
|
|
@@ -90,7 +90,11 @@ let WorkoutListPageService = (() => {
|
|
|
90
90
|
openPage() {
|
|
91
91
|
try {
|
|
92
92
|
super.openPage();
|
|
93
|
-
|
|
93
|
+
let pageType;
|
|
94
|
+
if (!this.getAppState().hasFeature('MOBILE_WORKOUTS')) {
|
|
95
|
+
pageType = 'placeholder';
|
|
96
|
+
}
|
|
97
|
+
this.logEvent({ message: 'page shown', page: 'Workouts', pageType });
|
|
94
98
|
const { observer } = this.getWorkoutList().open();
|
|
95
99
|
this.listObserver = observer;
|
|
96
100
|
this.listObserver?.on('started', this.listUpdateHandler);
|
|
@@ -320,10 +324,12 @@ let WorkoutListPageService = (() => {
|
|
|
320
324
|
this.importPhase = 'importing';
|
|
321
325
|
this.importingFileName = file?.name;
|
|
322
326
|
this.emitImportUpdate();
|
|
327
|
+
this.logEvent({ message: 'workout import attempted', file: file?.name });
|
|
323
328
|
this.getWorkoutList().import(file, { showImportCards: false })
|
|
324
329
|
.then(([card]) => {
|
|
325
330
|
const group = this.getLastUsedImportGroup();
|
|
326
331
|
observer.emit('success');
|
|
332
|
+
this.logEvent({ message: 'workout import succeeded', file: file?.name, workoutName: card.getTitle(), group });
|
|
327
333
|
if (generation !== this.importGeneration)
|
|
328
334
|
return;
|
|
329
335
|
if (group !== DEFAULT_IMPORT_GROUP)
|
|
@@ -334,6 +340,7 @@ let WorkoutListPageService = (() => {
|
|
|
334
340
|
})
|
|
335
341
|
.catch((err) => {
|
|
336
342
|
observer.emit('error', err);
|
|
343
|
+
this.logEvent({ message: 'workout import failed', file: file?.name, error: err?.message });
|
|
337
344
|
if (generation !== this.importGeneration)
|
|
338
345
|
return;
|
|
339
346
|
this.importPhase = 'error';
|
|
@@ -88,8 +88,8 @@ let WorkoutRidePageService = (() => {
|
|
|
88
88
|
this.workoutEventHandler['update'] = this.onWorkoutUpdate.bind(this);
|
|
89
89
|
this.workoutEventHandler['forward'] = this.onWorkoutUpdate.bind(this);
|
|
90
90
|
this.workoutEventHandler['backward'] = this.onWorkoutUpdate.bind(this);
|
|
91
|
-
this.workoutEventHandler['completed'] = this.
|
|
92
|
-
this.workoutEventHandler['stopped'] = this.
|
|
91
|
+
this.workoutEventHandler['completed'] = this.onFinished.bind(this);
|
|
92
|
+
this.workoutEventHandler['stopped'] = this.onFinished.bind(this);
|
|
93
93
|
}
|
|
94
94
|
openPage(simulate) {
|
|
95
95
|
try {
|
|
@@ -116,7 +116,7 @@ let WorkoutRidePageService = (() => {
|
|
|
116
116
|
try {
|
|
117
117
|
EventLogger.setGlobalConfig('page', null);
|
|
118
118
|
this.logEvent({ message: 'page closed', page: 'WorkoutRide' });
|
|
119
|
-
this.getRideDisplay().stop(
|
|
119
|
+
this.getRideDisplay().stop();
|
|
120
120
|
this.unregisterRideEventHandlers();
|
|
121
121
|
this.unsubscribeFromWorkoutObserver();
|
|
122
122
|
this.menuProps = null;
|
|
@@ -240,8 +240,7 @@ let WorkoutRidePageService = (() => {
|
|
|
240
240
|
}
|
|
241
241
|
onStop() {
|
|
242
242
|
try {
|
|
243
|
-
this.
|
|
244
|
-
this.emitNavigateBack();
|
|
243
|
+
this.finishRide();
|
|
245
244
|
}
|
|
246
245
|
catch (err) {
|
|
247
246
|
this.logError(err, 'onStop');
|
|
@@ -350,7 +349,17 @@ let WorkoutRidePageService = (() => {
|
|
|
350
349
|
onWorkoutUpdate() {
|
|
351
350
|
this.updatePageDisplay();
|
|
352
351
|
}
|
|
353
|
-
|
|
352
|
+
onFinished() {
|
|
353
|
+
try {
|
|
354
|
+
this.menuProps = { showResume: false, finished: true, canStepBack: false, canStepForward: false };
|
|
355
|
+
this.updatePageDisplay();
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
this.logError(err, 'onFinished');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
finishRide() {
|
|
362
|
+
this.getRideDisplay().stop(true);
|
|
354
363
|
this.emitNavigateBack();
|
|
355
364
|
}
|
|
356
365
|
subscribeToWorkoutObserver() {
|
|
@@ -6,7 +6,7 @@ import { ActivitiesRepository, ActivityConverterFactory, ActivityDetails, Activi
|
|
|
6
6
|
import { FreeRideStartSettings } from "../../routes/list/types";
|
|
7
7
|
import { RouteSettings } from "../../routes/list/cards/types";
|
|
8
8
|
import { RoutePoint } from "../../routes/base/types";
|
|
9
|
-
import { ActivityDashboardDataItem, ActivityState, ActivitySummaryDisplayProperties } from "./types";
|
|
9
|
+
import { ActivityDashboardDataItem, ActivityState, ActivitySummaryDisplayProperties, ActivityWorkoutSummaryGraph } from "./types";
|
|
10
10
|
import { Route } from "../../routes/base/model/route";
|
|
11
11
|
import { ActivityStatsCalculator } from "./stats";
|
|
12
12
|
import { ActivityDuration } from "./duration";
|
|
@@ -146,6 +146,7 @@ export declare class ActivityRideService extends IncyclistService {
|
|
|
146
146
|
elevationGain?: undefined;
|
|
147
147
|
};
|
|
148
148
|
getActivitySummaryDisplayProperties(): ActivitySummaryDisplayProperties;
|
|
149
|
+
protected buildWorkoutSummaryGraph(activity?: ActivityDetails): ActivityWorkoutSummaryGraph | undefined;
|
|
149
150
|
protected canShowDonate(): boolean;
|
|
150
151
|
onDonateClicked(): void;
|
|
151
152
|
getActivity(): ActivityDetails;
|
|
@@ -2,12 +2,19 @@ import { HealthStatus } from "../../devices";
|
|
|
2
2
|
import { Dimension, Unit } from "../../i18n";
|
|
3
3
|
import { RoutePoint } from "../../types";
|
|
4
4
|
import { ActivityDetailsUI } from "../base";
|
|
5
|
+
import { WorkoutGraphActuals, WorkoutGraphPlan } from "../../workouts";
|
|
5
6
|
export type ActivityState = 'ininitalized' | 'active' | 'paused' | 'completed' | 'idle';
|
|
7
|
+
export type ActivityWorkoutSummaryGraph = {
|
|
8
|
+
plan: WorkoutGraphPlan;
|
|
9
|
+
actuals: WorkoutGraphActuals;
|
|
10
|
+
};
|
|
6
11
|
export type ActivitySummaryDisplayProperties = {
|
|
7
12
|
activity?: ActivityDetailsUI;
|
|
8
13
|
showSave?: boolean;
|
|
9
14
|
showContinue?: boolean;
|
|
10
15
|
showMap?: boolean;
|
|
16
|
+
showWorkoutSummary?: boolean;
|
|
17
|
+
workoutGraph?: ActivityWorkoutSummaryGraph;
|
|
11
18
|
preview?: string;
|
|
12
19
|
units?: Record<Dimension, Unit>;
|
|
13
20
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { IMessageQueueBinding } from "../api/mq";
|
|
2
|
+
import { IncyclistService } from "../base/service";
|
|
3
|
+
import { UserSettingsService } from "../settings/service/service";
|
|
4
|
+
import { FeatureToggle } from "./types";
|
|
5
|
+
export declare class FeatureToggleSyncService extends IncyclistService {
|
|
6
|
+
protected uuid: string;
|
|
7
|
+
protected subscribed: boolean;
|
|
8
|
+
protected onMessageHandler: any;
|
|
9
|
+
constructor();
|
|
10
|
+
start(uuid: string): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
protected getTopic(uuid: string): string;
|
|
13
|
+
protected getTopicPrefix(): string | undefined;
|
|
14
|
+
protected onMessage(topic: string, message: string | Uint8Array): void;
|
|
15
|
+
protected parseToggleName(topic: string, prefix: string): FeatureToggle | undefined;
|
|
16
|
+
protected parsePayload(message: string | Uint8Array): {
|
|
17
|
+
value?: boolean;
|
|
18
|
+
} | undefined;
|
|
19
|
+
protected getMessageQueue(): IMessageQueueBinding;
|
|
20
|
+
protected getUserSettings(): UserSettingsService;
|
|
21
|
+
}
|
|
22
|
+
export declare const useFeatureToggleSync: () => FeatureToggleSyncService;
|
|
@@ -56,5 +56,6 @@ export declare class UserInterfaceServcie extends IncyclistService {
|
|
|
56
56
|
protected getUserSettings(): import("../settings").UserSettingsService;
|
|
57
57
|
protected getOnlineStatusMonitoring(): OnlineStateMonitoringService;
|
|
58
58
|
protected getAppState(): import("../appstate").AppStateService;
|
|
59
|
+
protected getFeatureToggleSync(): import("../appstate").FeatureToggleSyncService;
|
|
59
60
|
}
|
|
60
61
|
export declare const useIncyclist: () => UserInterfaceServcie;
|
|
@@ -40,7 +40,8 @@ export declare class WorkoutRidePageService extends IncyclistPageService impleme
|
|
|
40
40
|
adjustLoad(deltaPct: number): PowerAdjustmentResult | undefined;
|
|
41
41
|
protected onRideStateUpdate(state: CurrentRideState): void;
|
|
42
42
|
protected onWorkoutUpdate(): void;
|
|
43
|
-
protected
|
|
43
|
+
protected onFinished(): void;
|
|
44
|
+
protected finishRide(): void;
|
|
44
45
|
protected subscribeToWorkoutObserver(): void;
|
|
45
46
|
protected unsubscribeFromWorkoutObserver(): void;
|
|
46
47
|
protected registerRideEventHandlers(): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IObserver } from "../../../base/typedefs";
|
|
2
2
|
import type { IPageService } from "../../../base/pages";
|
|
3
|
-
import type { RidePageDisplayProps } from "../../../ride/page/types";
|
|
3
|
+
import type { RideMenuProps, RidePageDisplayProps } from "../../../ride/page/types";
|
|
4
4
|
import type { WorkoutGraphPlanBar } from "../../base/graph/types";
|
|
5
5
|
export interface WorkoutGraphPoint {
|
|
6
6
|
x: number;
|
|
@@ -37,8 +37,7 @@ export interface WorkoutDashboardLine {
|
|
|
37
37
|
text: string;
|
|
38
38
|
mode: string | null;
|
|
39
39
|
}
|
|
40
|
-
export interface WorkoutRideMenuProps {
|
|
41
|
-
showResume: boolean;
|
|
40
|
+
export interface WorkoutRideMenuProps extends RideMenuProps {
|
|
42
41
|
canStepBack: boolean;
|
|
43
42
|
canStepForward: boolean;
|
|
44
43
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incyclist-services",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.90",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"gd-eventlog": "^0.1.27"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@garmin/fitsdk": "^21.200.0",
|
|
9
9
|
"axios": "^1.15.2",
|
|
10
|
-
"incyclist-devices": "^3.0.
|
|
10
|
+
"incyclist-devices": "^3.0.26",
|
|
11
11
|
"promise.any": "^2.0.6",
|
|
12
12
|
"semver": "^7.7.4",
|
|
13
13
|
"tcx-builder": "^1.1.1",
|