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.
@@ -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 isWorkout = this.activity?.routeType === 'None';
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 || isWorkout || (route?.description?.hasGpx);
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) && c.selected !== udid) {
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 || !url.startsWith('video:'))
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 (country)', route: card?.getData()?.title });
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
  });
@@ -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
- const sent = false;
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
- this.logEvent({ message: 'page shown', page: 'Workouts' });
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.onWorkoutFinished.bind(this);
95
- this.workoutEventHandler['stopped'] = this.onWorkoutFinished.bind(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(true);
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.getRideDisplay().stop(true);
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
- onWorkoutFinished() {
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 isWorkout = this.activity?.routeType === 'None';
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 || isWorkout || (route?.description?.hasGpx);
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) {
@@ -1,2 +1,3 @@
1
1
  export * from './service';
2
2
  export * from './types';
3
+ export * from './toggle-sync';
@@ -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) && c.selected !== udid) {
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 || !url.startsWith('video:'))
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 (country)', route: card?.getData()?.title });
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
  });
@@ -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
- const sent = false;
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
- this.logEvent({ message: 'page shown', page: 'Workouts' });
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.onWorkoutFinished.bind(this);
92
- this.workoutEventHandler['stopped'] = this.onWorkoutFinished.bind(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(true);
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.getRideDisplay().stop(true);
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
- onWorkoutFinished() {
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
  };
@@ -1,2 +1,3 @@
1
1
  export * from './service';
2
2
  export * from './types';
3
+ export * from './toggle-sync';
@@ -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 onWorkoutFinished(): void;
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.88",
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.25",
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",