incyclist-services 1.7.71 → 1.7.72
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/devices/ride/service.js +7 -0
- package/lib/cjs/ride/display/service.js +4 -4
- package/lib/cjs/ride/page/service.js +18 -0
- package/lib/cjs/ride/route/RouteDisplayService.js +1 -1
- package/lib/esm/activities/upload/trainingpeaks.js +123 -0
- package/lib/esm/apps/base/api/trainingpeaks/api.js +143 -0
- package/lib/esm/apps/base/api/trainingpeaks/index.js +1 -0
- package/lib/esm/apps/base/api/trainingpeaks/types.js +1 -0
- package/lib/esm/apps/trainingpeaks/TrainingPeaksAppConnection.js +189 -0
- package/lib/esm/apps/trainingpeaks/types.js +1 -0
- package/lib/esm/devices/ride/service.js +7 -0
- package/lib/esm/ride/display/service.js +4 -4
- package/lib/esm/ride/page/service.js +18 -0
- package/lib/esm/ride/route/RouteDisplayService.js +1 -1
- package/lib/esm/workouts/calendar/sync/trainingpeaks/provider.js +201 -0
- package/lib/types/activities/upload/trainingpeaks.d.ts +16 -0
- package/lib/types/apps/base/api/trainingpeaks/api.d.ts +30 -0
- package/lib/types/apps/base/api/trainingpeaks/index.d.ts +2 -0
- package/lib/types/apps/base/api/trainingpeaks/types.d.ts +55 -0
- package/lib/types/apps/trainingpeaks/TrainingPeaksAppConnection.d.ts +22 -0
- package/lib/types/apps/trainingpeaks/types.d.ts +6 -0
- package/lib/types/ride/page/service.d.ts +1 -0
- package/lib/types/ride/page/types.d.ts +2 -1
- package/lib/types/workouts/calendar/sync/trainingpeaks/provider.d.ts +22 -0
- package/package.json +2 -2
|
@@ -1362,8 +1362,15 @@ let DeviceRideService = (() => {
|
|
|
1362
1362
|
canEnforceSimulator() {
|
|
1363
1363
|
const selected = (0, routes_1.getRouteList)().getSelected();
|
|
1364
1364
|
const personalApiKey = this.getGoogleMaps().hasPersonalApiKey();
|
|
1365
|
+
if (!selected?.description)
|
|
1366
|
+
return false;
|
|
1365
1367
|
if (selected?.description?.hasVideo || personalApiKey)
|
|
1366
1368
|
return true;
|
|
1369
|
+
if (!selected.description.hasVideo) {
|
|
1370
|
+
const rideView = (0, settings_1.useUserSettings)().getValue('preferences.rideView', null);
|
|
1371
|
+
if (rideView === 'map')
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1367
1374
|
return (0, settings_1.useUserSettings)().isNewUser();
|
|
1368
1375
|
}
|
|
1369
1376
|
logEvent(event) {
|
|
@@ -377,7 +377,7 @@ let RideDisplayService = (() => {
|
|
|
377
377
|
return { ...props, ...childProps };
|
|
378
378
|
}
|
|
379
379
|
catch (err) {
|
|
380
|
-
this.logError(err, 'getDisplayProperties');
|
|
380
|
+
this.logError(err, 'getDisplayProperties', { state: this.state });
|
|
381
381
|
return { state: this.state };
|
|
382
382
|
}
|
|
383
383
|
}
|
|
@@ -425,7 +425,7 @@ let RideDisplayService = (() => {
|
|
|
425
425
|
async stopRide(props = {}) {
|
|
426
426
|
try {
|
|
427
427
|
if (this.state !== 'Starting' && this.state !== 'Idle') {
|
|
428
|
-
this.logEvent({ message: "activity stopped", activity: this.activity?.id });
|
|
428
|
+
this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: this.state });
|
|
429
429
|
}
|
|
430
430
|
const prevState = this.state;
|
|
431
431
|
this.state = prevState === 'Starting' ? 'Closing' : 'Finished';
|
|
@@ -449,7 +449,6 @@ let RideDisplayService = (() => {
|
|
|
449
449
|
}
|
|
450
450
|
if (prevState !== 'Starting')
|
|
451
451
|
this.enableScreensaver();
|
|
452
|
-
this.state = 'Idle';
|
|
453
452
|
}
|
|
454
453
|
catch (err) {
|
|
455
454
|
this.logError(err, 'stopRide');
|
|
@@ -458,9 +457,10 @@ let RideDisplayService = (() => {
|
|
|
458
457
|
}
|
|
459
458
|
async closePrevRide() {
|
|
460
459
|
try {
|
|
461
|
-
if (this.observer || (this.state !== 'Idle')) {
|
|
460
|
+
if (this.observer || (this.state !== 'Idle' && this.state !== 'Finished')) {
|
|
462
461
|
await this.stopRide({ noStateUpdates: true });
|
|
463
462
|
}
|
|
463
|
+
this.state = 'Idle';
|
|
464
464
|
}
|
|
465
465
|
catch (err) {
|
|
466
466
|
this.logError(err, 'init');
|
|
@@ -196,6 +196,12 @@ let RidePageService = (() => {
|
|
|
196
196
|
}
|
|
197
197
|
onMenuClose() {
|
|
198
198
|
try {
|
|
199
|
+
const state = this.getRideDisplay().getState();
|
|
200
|
+
if (state === 'Finished' || this.menuProps.finished) {
|
|
201
|
+
this.moveToPreviousPage();
|
|
202
|
+
this.closePage();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
199
205
|
this.menuProps = null;
|
|
200
206
|
this.updatePageDisplay();
|
|
201
207
|
}
|
|
@@ -223,6 +229,15 @@ let RidePageService = (() => {
|
|
|
223
229
|
this.logError(err, 'onResume');
|
|
224
230
|
}
|
|
225
231
|
}
|
|
232
|
+
onFinished() {
|
|
233
|
+
try {
|
|
234
|
+
this.menuProps = { showResume: false, finished: true };
|
|
235
|
+
this.updatePageDisplay();
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
this.logError(err, 'onResume');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
226
241
|
onEndRide() {
|
|
227
242
|
try {
|
|
228
243
|
this.getRideDisplay().stop();
|
|
@@ -336,6 +351,9 @@ let RidePageService = (() => {
|
|
|
336
351
|
case 'Paused':
|
|
337
352
|
this.menuProps = { showResume: true };
|
|
338
353
|
break;
|
|
354
|
+
case 'Finished':
|
|
355
|
+
this.menuProps = { showResume: false, finished: true };
|
|
356
|
+
break;
|
|
339
357
|
case 'Active':
|
|
340
358
|
this.menuProps = null;
|
|
341
359
|
break;
|
|
@@ -276,7 +276,7 @@ let RouteDisplayService = (() => {
|
|
|
276
276
|
const mode = this.getDeviceRide().getCyclingMode();
|
|
277
277
|
const isSIM = mode?.isSIM();
|
|
278
278
|
const realityFactor = this.startSettings?.realityFactor ?? 100;
|
|
279
|
-
const targetSlope = (this.position
|
|
279
|
+
const targetSlope = (this.position?.slope ?? 0) * realityFactor / 100;
|
|
280
280
|
if (props?.limits && !isSIM) {
|
|
281
281
|
delete this.prevRequestSlope;
|
|
282
282
|
return { ...props?.limits, slope: targetSlope };
|
|
@@ -0,0 +1,123 @@
|
|
|
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 { Injectable } from '../../base/decorators/Injection';
|
|
36
|
+
import { IncyclistService } from '../../base/service';
|
|
37
|
+
import { Singleton } from '../../base/types';
|
|
38
|
+
import { TrainingPeaksAppConnection } from '../../apps/trainingpeaks/TrainingPeaksAppConnection';
|
|
39
|
+
import { ActivityUploadFactory } from './factory';
|
|
40
|
+
let TrainingPeaksUpload = (() => {
|
|
41
|
+
let _classDecorators = [Singleton];
|
|
42
|
+
let _classDescriptor;
|
|
43
|
+
let _classExtraInitializers = [];
|
|
44
|
+
let _classThis;
|
|
45
|
+
let _classSuper = IncyclistService;
|
|
46
|
+
let _instanceExtraInitializers = [];
|
|
47
|
+
let _getConnection_decorators;
|
|
48
|
+
var TrainingPeaksUpload = class extends _classSuper {
|
|
49
|
+
static { _classThis = this; }
|
|
50
|
+
static {
|
|
51
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
52
|
+
_getConnection_decorators = [Injectable];
|
|
53
|
+
__esDecorate(this, null, _getConnection_decorators, { kind: "method", name: "getConnection", static: false, private: false, access: { has: obj => "getConnection" in obj, get: obj => obj.getConnection }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
54
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
55
|
+
TrainingPeaksUpload = _classThis = _classDescriptor.value;
|
|
56
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
57
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
58
|
+
}
|
|
59
|
+
isInitialized = __runInitializers(this, _instanceExtraInitializers);
|
|
60
|
+
connection = new TrainingPeaksAppConnection();
|
|
61
|
+
constructor() {
|
|
62
|
+
super('TrainingPeaksUpload');
|
|
63
|
+
this.isInitialized = false;
|
|
64
|
+
this.init();
|
|
65
|
+
}
|
|
66
|
+
init() {
|
|
67
|
+
if (this.isInitialized)
|
|
68
|
+
return true;
|
|
69
|
+
this.isInitialized = this.getConnection().init();
|
|
70
|
+
return this.isInitialized;
|
|
71
|
+
}
|
|
72
|
+
isConnected() {
|
|
73
|
+
return this.getConnection().isConnected();
|
|
74
|
+
}
|
|
75
|
+
async upload(activity, format = 'TCX') {
|
|
76
|
+
try {
|
|
77
|
+
const ok = this.ensureInitialized();
|
|
78
|
+
if (!ok) {
|
|
79
|
+
this.logEvent({ message: 'TrainingPeaks upload skipped', reason: 'not initialized' });
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!this.isConnected())
|
|
83
|
+
return false;
|
|
84
|
+
const lcFormat = format.toLowerCase();
|
|
85
|
+
const ucFormat = format.toUpperCase();
|
|
86
|
+
this.logEvent({ message: 'TrainingPeaks upload', format: ucFormat });
|
|
87
|
+
if (!activity.links)
|
|
88
|
+
activity.links = {};
|
|
89
|
+
const fileName = activity[`${lcFormat}FileName`];
|
|
90
|
+
const res = await this.getApi().upload(fileName, { name: activity.title, description: '' });
|
|
91
|
+
this.logEvent({ message: 'TrainingPeaks upload success', activityId: res.trainingPeaksId });
|
|
92
|
+
activity.links.trainingpeaks = {
|
|
93
|
+
activity_id: String(res.trainingPeaksId),
|
|
94
|
+
url: this.getUrl(res.trainingPeaksId)
|
|
95
|
+
};
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.logEvent({ message: 'TrainingPeaks upload failure', error: err.message });
|
|
100
|
+
activity.links.trainingpeaks = { error: err.message };
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
getUrl(activityId) {
|
|
105
|
+
return `https://www.trainingpeaks.com/athlete/workouts/${activityId}`;
|
|
106
|
+
}
|
|
107
|
+
ensureInitialized() {
|
|
108
|
+
if (!this.isInitialized)
|
|
109
|
+
this.init();
|
|
110
|
+
return this.isInitialized;
|
|
111
|
+
}
|
|
112
|
+
getConnection() {
|
|
113
|
+
return this.connection;
|
|
114
|
+
}
|
|
115
|
+
getApi() {
|
|
116
|
+
return this.getConnection().getApi();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return TrainingPeaksUpload = _classThis;
|
|
120
|
+
})();
|
|
121
|
+
export { TrainingPeaksUpload };
|
|
122
|
+
const factory = new ActivityUploadFactory();
|
|
123
|
+
factory.add('trainingpeaks', new TrainingPeaksUpload());
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { EventLogger } from 'gd-eventlog';
|
|
2
|
+
import { Observer } from '../../../../base/types/observer';
|
|
3
|
+
import { valid } from '../../../../utils/valid';
|
|
4
|
+
import { sleep } from '../../../../utils/sleep';
|
|
5
|
+
import { AppApiBase } from '../base';
|
|
6
|
+
const UPLOAD_POLL_INTERVAL_MS = 2000;
|
|
7
|
+
const UPLOAD_TIMEOUT_MS = 60000;
|
|
8
|
+
export class TrainingPeaksApi extends AppApiBase {
|
|
9
|
+
config;
|
|
10
|
+
logger;
|
|
11
|
+
observer;
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.logger = new EventLogger('TrainingPeaksApi');
|
|
15
|
+
this.observer = new Observer();
|
|
16
|
+
}
|
|
17
|
+
getBaseUrl() {
|
|
18
|
+
return process.env.TRAININGPEAKS_API || 'https://api.sandbox.trainingpeaks.com';
|
|
19
|
+
}
|
|
20
|
+
getOauthBaseUrl() {
|
|
21
|
+
return process.env.TRAININGPEAKS_OAUTH_URL || 'https://oauth.sandbox.trainingpeaks.com';
|
|
22
|
+
}
|
|
23
|
+
init(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
return this.observer;
|
|
26
|
+
}
|
|
27
|
+
update(config) {
|
|
28
|
+
if (!this.config)
|
|
29
|
+
throw new Error('update not allowed before init');
|
|
30
|
+
this.config = { ...this.config, ...config };
|
|
31
|
+
}
|
|
32
|
+
isAuthenticated() {
|
|
33
|
+
return valid(this.config?.accessToken);
|
|
34
|
+
}
|
|
35
|
+
async getLoggedInAthlete() {
|
|
36
|
+
await this.verifyAuthentication();
|
|
37
|
+
const response = await this.get('/v1/athlete/profile');
|
|
38
|
+
return response.data;
|
|
39
|
+
}
|
|
40
|
+
async getCalendarWorkouts(opts) {
|
|
41
|
+
await this.verifyAuthentication();
|
|
42
|
+
const oldest = opts?.oldest ?? new Date();
|
|
43
|
+
const newest = opts?.newest ?? new Date(oldest.valueOf() + 30 * 24 * 60 * 60 * 1000);
|
|
44
|
+
const fmt = (d) => d.toISOString().slice(0, 10);
|
|
45
|
+
const response = await this.get(`/v2/workouts/${fmt(oldest)}/${fmt(newest)}`);
|
|
46
|
+
return response.data;
|
|
47
|
+
}
|
|
48
|
+
async getWorkoutFile(id, format = 'zwo') {
|
|
49
|
+
await this.verifyAuthentication();
|
|
50
|
+
const response = await this.get(`/v2/workouts/wod/file/${id}?format=${format}`);
|
|
51
|
+
return response.data;
|
|
52
|
+
}
|
|
53
|
+
async upload(fileName, props) {
|
|
54
|
+
await this.verifyAuthentication();
|
|
55
|
+
const reqOpts = {
|
|
56
|
+
headers: { Authorization: `bearer ${this.config.accessToken}` }
|
|
57
|
+
};
|
|
58
|
+
const form = await this.createForm('/v3/file', { file: { type: 'file', fileName } }, reqOpts);
|
|
59
|
+
const response = await this.postForm(form);
|
|
60
|
+
const location = response.headers?.['location'] ?? '';
|
|
61
|
+
const trackingId = location.split('/').filter(Boolean).pop();
|
|
62
|
+
if (!trackingId) {
|
|
63
|
+
throw new Error('TrainingPeaks upload failed: no tracking ID returned');
|
|
64
|
+
}
|
|
65
|
+
return this.waitForUploadComplete(trackingId, props?.externalId);
|
|
66
|
+
}
|
|
67
|
+
async getUploadStatus(trackingId) {
|
|
68
|
+
await this.verifyAuthentication();
|
|
69
|
+
const response = await this.get(`/v3/status/${trackingId}`);
|
|
70
|
+
return response.data;
|
|
71
|
+
}
|
|
72
|
+
async verifyAuthentication() {
|
|
73
|
+
if (!this.isAuthenticated())
|
|
74
|
+
throw new Error('not authenticated');
|
|
75
|
+
await this.verifyToken();
|
|
76
|
+
}
|
|
77
|
+
async verifyToken() {
|
|
78
|
+
if (!this.isTokenStillValid()) {
|
|
79
|
+
try {
|
|
80
|
+
await this.refreshToken();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new Error('token refresh failed');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
isTokenStillValid() {
|
|
88
|
+
if (!this.config?.expiration)
|
|
89
|
+
return false;
|
|
90
|
+
return Date.now() < this.config.expiration.valueOf();
|
|
91
|
+
}
|
|
92
|
+
async refreshToken() {
|
|
93
|
+
this.logger.logEvent({ message: 'TrainingPeaks refresh token start' });
|
|
94
|
+
try {
|
|
95
|
+
const response = await this.getApi().request({
|
|
96
|
+
method: 'post',
|
|
97
|
+
url: `${this.getOauthBaseUrl()}/oauth/token`,
|
|
98
|
+
data: new URLSearchParams({
|
|
99
|
+
grant_type: 'refresh_token',
|
|
100
|
+
refresh_token: this.config.refreshToken,
|
|
101
|
+
client_id: this.config.clientId,
|
|
102
|
+
client_secret: this.config.clientSecret,
|
|
103
|
+
}).toString(),
|
|
104
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
105
|
+
});
|
|
106
|
+
const data = response.data;
|
|
107
|
+
this.config.accessToken = data.access_token;
|
|
108
|
+
this.config.refreshToken = data.refresh_token;
|
|
109
|
+
this.config.expiration = new Date(Date.now() + data.expires_in * 1000);
|
|
110
|
+
this.logger.logEvent({ message: 'TrainingPeaks refresh token success' });
|
|
111
|
+
this.observer.emit('token.updated', this.config);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
this.logger.logEvent({ message: 'TrainingPeaks refresh token error', error: err.message });
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async waitForUploadComplete(trackingId, externalId) {
|
|
119
|
+
const deadline = Date.now() + UPLOAD_TIMEOUT_MS;
|
|
120
|
+
while (Date.now() < deadline) {
|
|
121
|
+
const status = await this.getUploadStatus(trackingId);
|
|
122
|
+
if (status.Completed) {
|
|
123
|
+
if (!status.WorkoutIds?.length) {
|
|
124
|
+
throw new Error(`TrainingPeaks upload failed: ${status.Status}`);
|
|
125
|
+
}
|
|
126
|
+
return { trainingPeaksId: status.WorkoutIds[0], externalId };
|
|
127
|
+
}
|
|
128
|
+
await sleep(UPLOAD_POLL_INTERVAL_MS);
|
|
129
|
+
}
|
|
130
|
+
throw new Error('TrainingPeaks upload timed out');
|
|
131
|
+
}
|
|
132
|
+
async get(url, config) {
|
|
133
|
+
const props = config ?? {};
|
|
134
|
+
const response = await this.getApi().request({
|
|
135
|
+
method: 'get',
|
|
136
|
+
url: this.getBaseUrl() + url,
|
|
137
|
+
headers: { Authorization: `bearer ${this.config.accessToken}` },
|
|
138
|
+
validateStatus: (status) => (status >= 200 && status < 300) || status === 403,
|
|
139
|
+
...props
|
|
140
|
+
});
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TrainingPeaksApi } from './api';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,189 @@
|
|
|
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, Singleton } from '../../base/decorators';
|
|
37
|
+
import { valid } from '../../utils/valid';
|
|
38
|
+
import { TrainingPeaksApi } from '../base/api/trainingpeaks';
|
|
39
|
+
import { ConnectedAppService } from '../base/app';
|
|
40
|
+
let TrainingPeaksAppConnection = (() => {
|
|
41
|
+
let _classDecorators = [Singleton];
|
|
42
|
+
let _classDescriptor;
|
|
43
|
+
let _classExtraInitializers = [];
|
|
44
|
+
let _classThis;
|
|
45
|
+
let _classSuper = ConnectedAppService;
|
|
46
|
+
let _instanceExtraInitializers = [];
|
|
47
|
+
let _getSecretBindings_decorators;
|
|
48
|
+
var TrainingPeaksAppConnection = class extends _classSuper {
|
|
49
|
+
static { _classThis = this; }
|
|
50
|
+
static {
|
|
51
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
52
|
+
_getSecretBindings_decorators = [Injectable];
|
|
53
|
+
__esDecorate(this, null, _getSecretBindings_decorators, { kind: "method", name: "getSecretBindings", static: false, private: false, access: { has: obj => "getSecretBindings" in obj, get: obj => obj.getSecretBindings }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
54
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
55
|
+
TrainingPeaksAppConnection = _classThis = _classDescriptor.value;
|
|
56
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
57
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
58
|
+
}
|
|
59
|
+
config = __runInitializers(this, _instanceExtraInitializers);
|
|
60
|
+
api;
|
|
61
|
+
tokenUpdateHandler = this.updateConfig.bind(this);
|
|
62
|
+
constructor() {
|
|
63
|
+
super('TrainingPeaksAppConnection', 'trainingpeaks');
|
|
64
|
+
}
|
|
65
|
+
async connect(credentials) {
|
|
66
|
+
this.ensureInitialized();
|
|
67
|
+
this.logEvent({ message: 'Connect with TrainingPeaks' });
|
|
68
|
+
try {
|
|
69
|
+
const isConnected = this.isConnected();
|
|
70
|
+
const config = this.buildConfigFromCredentials(credentials);
|
|
71
|
+
if (isConnected) {
|
|
72
|
+
this.getApi().update(config);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
this.initApi(config);
|
|
76
|
+
this.saveCredentials(config);
|
|
77
|
+
}
|
|
78
|
+
this.logEvent({ message: 'Connect with TrainingPeaks success' });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
this.logError(err, 'connect');
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
isConnecting() {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
disconnect() {
|
|
90
|
+
try {
|
|
91
|
+
this.config = undefined;
|
|
92
|
+
this.getApi().init(undefined);
|
|
93
|
+
this.saveCredentials();
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
this.logError(err, 'disconnect');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
isConnected() {
|
|
100
|
+
if (!this.isInitialized) {
|
|
101
|
+
this.init();
|
|
102
|
+
}
|
|
103
|
+
return valid(this.config);
|
|
104
|
+
}
|
|
105
|
+
getCredentials() {
|
|
106
|
+
try {
|
|
107
|
+
return this.getUserSettings().get('user.auth.trainingpeaks', null);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
getApi() {
|
|
114
|
+
if (!this.api)
|
|
115
|
+
this.api = new TrainingPeaksApi();
|
|
116
|
+
return this.api;
|
|
117
|
+
}
|
|
118
|
+
initAuth() {
|
|
119
|
+
let isInitialized = false;
|
|
120
|
+
try {
|
|
121
|
+
const auth = this.getCredentials();
|
|
122
|
+
const clientId = this.getSecret('TRAININGPEAKS_CLIENT_ID');
|
|
123
|
+
const clientSecret = this.getSecret('TRAININGPEAKS_CLIENT_SECRET');
|
|
124
|
+
if (clientId && clientSecret && auth) {
|
|
125
|
+
this.logEvent({ message: 'TrainingPeaks init done', hasCredentials: true });
|
|
126
|
+
this.config = this.buildConfigFromCredentials(auth);
|
|
127
|
+
this.initApi(this.config);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
this.logEvent({ message: 'TrainingPeaks init done', hasCredentials: false });
|
|
131
|
+
}
|
|
132
|
+
isInitialized = true;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.logEvent({ message: 'error', error: err.message, fn: 'init', stack: err.stack });
|
|
136
|
+
isInitialized = false;
|
|
137
|
+
delete this.config;
|
|
138
|
+
}
|
|
139
|
+
return isInitialized;
|
|
140
|
+
}
|
|
141
|
+
updateConfig(config) {
|
|
142
|
+
const { accessToken, refreshToken, expiration } = config;
|
|
143
|
+
this.config = { ...this.config, accessToken, refreshToken, expiration };
|
|
144
|
+
this.saveCredentials();
|
|
145
|
+
}
|
|
146
|
+
saveCredentials(config) {
|
|
147
|
+
if (config)
|
|
148
|
+
this.config = config;
|
|
149
|
+
try {
|
|
150
|
+
if (!this.isConnected()) {
|
|
151
|
+
this.logEvent({ message: 'TrainingPeaks delete credentials' });
|
|
152
|
+
this.getUserSettings().set('user.auth.trainingpeaks', null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.logEvent({ message: 'TrainingPeaks save credentials' });
|
|
156
|
+
this.getUserSettings().set('user.auth.trainingpeaks', {
|
|
157
|
+
accesstoken: this.config.accessToken,
|
|
158
|
+
refreshtoken: this.config.refreshToken,
|
|
159
|
+
expiration: this.config.expiration?.valueOf().toString()
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
this.logEvent({ message: 'error', fn: 'saveCredentials', error: err.message, stack: err.stack });
|
|
164
|
+
}
|
|
165
|
+
this.logEvent({ message: 'TrainingPeaks save credentials done' });
|
|
166
|
+
}
|
|
167
|
+
buildConfigFromCredentials(credentials) {
|
|
168
|
+
return {
|
|
169
|
+
clientId: this.getSecret('TRAININGPEAKS_CLIENT_ID'),
|
|
170
|
+
clientSecret: this.getSecret('TRAININGPEAKS_CLIENT_SECRET'),
|
|
171
|
+
accessToken: credentials?.accesstoken,
|
|
172
|
+
refreshToken: credentials?.refreshtoken,
|
|
173
|
+
expiration: credentials?.expiration ? new Date(Number(credentials.expiration)) : undefined
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
initApi(config) {
|
|
177
|
+
const observer = this.getApi().init(config);
|
|
178
|
+
observer.on('token.updated', this.tokenUpdateHandler);
|
|
179
|
+
}
|
|
180
|
+
getSecret(key) {
|
|
181
|
+
return this.getSecretBindings()?.getSecret(key);
|
|
182
|
+
}
|
|
183
|
+
getSecretBindings() {
|
|
184
|
+
return getBindings()?.secret;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
return TrainingPeaksAppConnection = _classThis;
|
|
188
|
+
})();
|
|
189
|
+
export { TrainingPeaksAppConnection };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1356,8 +1356,15 @@ let DeviceRideService = (() => {
|
|
|
1356
1356
|
canEnforceSimulator() {
|
|
1357
1357
|
const selected = getRouteList().getSelected();
|
|
1358
1358
|
const personalApiKey = this.getGoogleMaps().hasPersonalApiKey();
|
|
1359
|
+
if (!selected?.description)
|
|
1360
|
+
return false;
|
|
1359
1361
|
if (selected?.description?.hasVideo || personalApiKey)
|
|
1360
1362
|
return true;
|
|
1363
|
+
if (!selected.description.hasVideo) {
|
|
1364
|
+
const rideView = useUserSettings().getValue('preferences.rideView', null);
|
|
1365
|
+
if (rideView === 'map')
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1361
1368
|
return useUserSettings().isNewUser();
|
|
1362
1369
|
}
|
|
1363
1370
|
logEvent(event) {
|
|
@@ -374,7 +374,7 @@ let RideDisplayService = (() => {
|
|
|
374
374
|
return { ...props, ...childProps };
|
|
375
375
|
}
|
|
376
376
|
catch (err) {
|
|
377
|
-
this.logError(err, 'getDisplayProperties');
|
|
377
|
+
this.logError(err, 'getDisplayProperties', { state: this.state });
|
|
378
378
|
return { state: this.state };
|
|
379
379
|
}
|
|
380
380
|
}
|
|
@@ -422,7 +422,7 @@ let RideDisplayService = (() => {
|
|
|
422
422
|
async stopRide(props = {}) {
|
|
423
423
|
try {
|
|
424
424
|
if (this.state !== 'Starting' && this.state !== 'Idle') {
|
|
425
|
-
this.logEvent({ message: "activity stopped", activity: this.activity?.id });
|
|
425
|
+
this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: this.state });
|
|
426
426
|
}
|
|
427
427
|
const prevState = this.state;
|
|
428
428
|
this.state = prevState === 'Starting' ? 'Closing' : 'Finished';
|
|
@@ -446,7 +446,6 @@ let RideDisplayService = (() => {
|
|
|
446
446
|
}
|
|
447
447
|
if (prevState !== 'Starting')
|
|
448
448
|
this.enableScreensaver();
|
|
449
|
-
this.state = 'Idle';
|
|
450
449
|
}
|
|
451
450
|
catch (err) {
|
|
452
451
|
this.logError(err, 'stopRide');
|
|
@@ -455,9 +454,10 @@ let RideDisplayService = (() => {
|
|
|
455
454
|
}
|
|
456
455
|
async closePrevRide() {
|
|
457
456
|
try {
|
|
458
|
-
if (this.observer || (this.state !== 'Idle')) {
|
|
457
|
+
if (this.observer || (this.state !== 'Idle' && this.state !== 'Finished')) {
|
|
459
458
|
await this.stopRide({ noStateUpdates: true });
|
|
460
459
|
}
|
|
460
|
+
this.state = 'Idle';
|
|
461
461
|
}
|
|
462
462
|
catch (err) {
|
|
463
463
|
this.logError(err, 'init');
|
|
@@ -193,6 +193,12 @@ let RidePageService = (() => {
|
|
|
193
193
|
}
|
|
194
194
|
onMenuClose() {
|
|
195
195
|
try {
|
|
196
|
+
const state = this.getRideDisplay().getState();
|
|
197
|
+
if (state === 'Finished' || this.menuProps.finished) {
|
|
198
|
+
this.moveToPreviousPage();
|
|
199
|
+
this.closePage();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
196
202
|
this.menuProps = null;
|
|
197
203
|
this.updatePageDisplay();
|
|
198
204
|
}
|
|
@@ -220,6 +226,15 @@ let RidePageService = (() => {
|
|
|
220
226
|
this.logError(err, 'onResume');
|
|
221
227
|
}
|
|
222
228
|
}
|
|
229
|
+
onFinished() {
|
|
230
|
+
try {
|
|
231
|
+
this.menuProps = { showResume: false, finished: true };
|
|
232
|
+
this.updatePageDisplay();
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
this.logError(err, 'onResume');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
223
238
|
onEndRide() {
|
|
224
239
|
try {
|
|
225
240
|
this.getRideDisplay().stop();
|
|
@@ -333,6 +348,9 @@ let RidePageService = (() => {
|
|
|
333
348
|
case 'Paused':
|
|
334
349
|
this.menuProps = { showResume: true };
|
|
335
350
|
break;
|
|
351
|
+
case 'Finished':
|
|
352
|
+
this.menuProps = { showResume: false, finished: true };
|
|
353
|
+
break;
|
|
336
354
|
case 'Active':
|
|
337
355
|
this.menuProps = null;
|
|
338
356
|
break;
|
|
@@ -270,7 +270,7 @@ let RouteDisplayService = (() => {
|
|
|
270
270
|
const mode = this.getDeviceRide().getCyclingMode();
|
|
271
271
|
const isSIM = mode?.isSIM();
|
|
272
272
|
const realityFactor = this.startSettings?.realityFactor ?? 100;
|
|
273
|
-
const targetSlope = (this.position
|
|
273
|
+
const targetSlope = (this.position?.slope ?? 0) * realityFactor / 100;
|
|
274
274
|
if (props?.limits && !isSIM) {
|
|
275
275
|
delete this.prevRequestSlope;
|
|
276
276
|
return { ...props?.limits, slope: targetSlope };
|
|
@@ -0,0 +1,201 @@
|
|
|
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 { useAppsService } from '../../../../apps';
|
|
36
|
+
import { TrainingPeaksAppConnection } from '../../../../apps/trainingpeaks/TrainingPeaksAppConnection';
|
|
37
|
+
import { Injectable } from '../../../../base/decorators';
|
|
38
|
+
import { IncyclistService } from '../../../../base/service';
|
|
39
|
+
import { Observer } from '../../../../base/types';
|
|
40
|
+
import { getFirstDayOfCurrentWeek, waitNextTick } from '../../../../utils';
|
|
41
|
+
import { ZwoParser } from '../../../base/parsers/zwo/zwo';
|
|
42
|
+
import { WorkoutSyncFactory } from '../factory';
|
|
43
|
+
const SYNC_DAYS = 30;
|
|
44
|
+
let TrainingPeaksCalendarSyncProvider = (() => {
|
|
45
|
+
let _classSuper = IncyclistService;
|
|
46
|
+
let _instanceExtraInitializers = [];
|
|
47
|
+
let _getApi_decorators;
|
|
48
|
+
let _getConnection_decorators;
|
|
49
|
+
let _getAppsService_decorators;
|
|
50
|
+
return class TrainingPeaksCalendarSyncProvider extends _classSuper {
|
|
51
|
+
static {
|
|
52
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
53
|
+
_getApi_decorators = [Injectable];
|
|
54
|
+
_getConnection_decorators = [Injectable];
|
|
55
|
+
_getAppsService_decorators = [Injectable];
|
|
56
|
+
__esDecorate(this, null, _getApi_decorators, { kind: "method", name: "getApi", static: false, private: false, access: { has: obj => "getApi" in obj, get: obj => obj.getApi }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
57
|
+
__esDecorate(this, null, _getConnection_decorators, { kind: "method", name: "getConnection", static: false, private: false, access: { has: obj => "getConnection" in obj, get: obj => obj.getConnection }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
58
|
+
__esDecorate(this, null, _getAppsService_decorators, { kind: "method", name: "getAppsService", static: false, private: false, access: { has: obj => "getAppsService" in obj, get: obj => obj.getAppsService }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
59
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
60
|
+
}
|
|
61
|
+
lastSyncTS = __runInitializers(this, _instanceExtraInitializers);
|
|
62
|
+
observer;
|
|
63
|
+
stopRequested;
|
|
64
|
+
workouts;
|
|
65
|
+
constructor() {
|
|
66
|
+
super('TrainingPeaksCalendarSync');
|
|
67
|
+
this.lastSyncTS = 0;
|
|
68
|
+
this.workouts = [];
|
|
69
|
+
}
|
|
70
|
+
sync() {
|
|
71
|
+
if (this.observer)
|
|
72
|
+
return this.observer;
|
|
73
|
+
this.observer = new Observer();
|
|
74
|
+
const onDone = () => {
|
|
75
|
+
waitNextTick().then(() => {
|
|
76
|
+
this.stopRequested = false;
|
|
77
|
+
delete this.observer;
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
this.observer.once('done', onDone);
|
|
81
|
+
this.observer.once('loaded', onDone);
|
|
82
|
+
this.observer.once('stop', () => { this.stopSync(); });
|
|
83
|
+
this.loadWorkouts();
|
|
84
|
+
return this.observer;
|
|
85
|
+
}
|
|
86
|
+
isConnected() {
|
|
87
|
+
try {
|
|
88
|
+
if (this.getApi().isAuthenticated())
|
|
89
|
+
return true;
|
|
90
|
+
return this.getConnection().isConnected();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
reset() {
|
|
97
|
+
this.lastSyncTS = 0;
|
|
98
|
+
this.workouts = [];
|
|
99
|
+
}
|
|
100
|
+
stopSync() {
|
|
101
|
+
this.stopRequested = true;
|
|
102
|
+
}
|
|
103
|
+
async loadWorkouts() {
|
|
104
|
+
const isFirst = this.lastSyncTS === 0;
|
|
105
|
+
const event = isFirst ? 'loaded' : 'done';
|
|
106
|
+
try {
|
|
107
|
+
if (!this.isConnected()) {
|
|
108
|
+
this.observer.emit(event, [], 'trainingpeaks');
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
if (!this.getAppsService().isEnabled('trainingpeaks', 'WorkoutDownload')) {
|
|
112
|
+
await waitNextTick();
|
|
113
|
+
this.observer.emit(event, [], 'trainingpeaks');
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const oldest = getFirstDayOfCurrentWeek();
|
|
117
|
+
const newest = new Date(oldest.valueOf() + SYNC_DAYS * 24 * 60 * 60 * 1000);
|
|
118
|
+
const all = await this.getApi().getCalendarWorkouts({ oldest, newest }) ?? [];
|
|
119
|
+
const bike = all.filter(w => w.WorkoutType === 'Bike');
|
|
120
|
+
await this.parseWorkouts(bike);
|
|
121
|
+
this.observer.emit(event, this.workouts, 'trainingpeaks');
|
|
122
|
+
this.lastSyncTS = Date.now();
|
|
123
|
+
return this.workouts;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
this.observer.emit(event, [], 'trainingpeaks');
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async parseWorkouts(workouts) {
|
|
131
|
+
const zwoParser = new ZwoParser();
|
|
132
|
+
const promises = [];
|
|
133
|
+
const transform = async (w) => {
|
|
134
|
+
try {
|
|
135
|
+
const day = new Date(w.WorkoutDay);
|
|
136
|
+
const updated = w.LastModifiedDate ? new Date(w.LastModifiedDate) : undefined;
|
|
137
|
+
if (!w.WorkoutFileFormats?.includes('zwo')) {
|
|
138
|
+
this.logEvent({
|
|
139
|
+
message: 'workout skipped - no ZWO format available',
|
|
140
|
+
workoutId: w.Id,
|
|
141
|
+
title: w.Title,
|
|
142
|
+
availableFormats: w.WorkoutFileFormats ?? []
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const fileContent = await this.getApi().getWorkoutFile(w.Id, 'zwo');
|
|
147
|
+
const workout = await zwoParser.fromStr(fileContent, w.Title);
|
|
148
|
+
const entry = {
|
|
149
|
+
day,
|
|
150
|
+
updated,
|
|
151
|
+
workoutId: String(w.Id),
|
|
152
|
+
workout
|
|
153
|
+
};
|
|
154
|
+
const existingIdx = this.workouts.findIndex(e => e.workoutId === String(w.Id));
|
|
155
|
+
const isExisting = existingIdx !== -1;
|
|
156
|
+
if (this.lastSyncTS > 0) {
|
|
157
|
+
if (!isExisting) {
|
|
158
|
+
this.observer.emit('added', entry, 'trainingpeaks');
|
|
159
|
+
}
|
|
160
|
+
else if (updated && updated.valueOf() > this.lastSyncTS) {
|
|
161
|
+
this.observer.emit('updated', entry, 'trainingpeaks');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (isExisting) {
|
|
165
|
+
this.workouts[existingIdx] = entry;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this.workouts.push(entry);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
this.logEvent({ message: 'error parsing workout', error: err.message, workoutId: w.Id, title: w.Title });
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
if (this.lastSyncTS > 0) {
|
|
176
|
+
const deleted = this.workouts.filter(e => !workouts.some(w => String(w.Id) === e.workoutId));
|
|
177
|
+
for (const e of deleted) {
|
|
178
|
+
this.observer.emit('deleted', e, 'trainingpeaks');
|
|
179
|
+
}
|
|
180
|
+
this.workouts = this.workouts.filter(e => workouts.some(w => String(w.Id) === e.workoutId));
|
|
181
|
+
}
|
|
182
|
+
for (const w of workouts) {
|
|
183
|
+
promises.push(transform(w));
|
|
184
|
+
}
|
|
185
|
+
await Promise.all(promises);
|
|
186
|
+
return this.workouts;
|
|
187
|
+
}
|
|
188
|
+
getApi() {
|
|
189
|
+
return this.getConnection().getApi();
|
|
190
|
+
}
|
|
191
|
+
getConnection() {
|
|
192
|
+
return new TrainingPeaksAppConnection();
|
|
193
|
+
}
|
|
194
|
+
getAppsService() {
|
|
195
|
+
return useAppsService();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
})();
|
|
199
|
+
export { TrainingPeaksCalendarSyncProvider };
|
|
200
|
+
const factory = new WorkoutSyncFactory();
|
|
201
|
+
factory.add('trainingpeaks', new TrainingPeaksCalendarSyncProvider());
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IncyclistService } from '../../base/service';
|
|
2
|
+
import { TrainingPeaksAppConnection } from '../../apps/trainingpeaks/TrainingPeaksAppConnection';
|
|
3
|
+
import { ActivityDetails } from '../base';
|
|
4
|
+
import { IActivityUpload } from './types';
|
|
5
|
+
export declare class TrainingPeaksUpload extends IncyclistService implements IActivityUpload {
|
|
6
|
+
protected isInitialized: boolean;
|
|
7
|
+
protected connection: TrainingPeaksAppConnection;
|
|
8
|
+
constructor();
|
|
9
|
+
init(): boolean;
|
|
10
|
+
isConnected(): boolean;
|
|
11
|
+
upload(activity: ActivityDetails, format?: string): Promise<boolean>;
|
|
12
|
+
getUrl(activityId: number | string): string;
|
|
13
|
+
protected ensureInitialized(): boolean;
|
|
14
|
+
protected getConnection(): TrainingPeaksAppConnection;
|
|
15
|
+
protected getApi(): import("../../apps/base/api/trainingpeaks").TrainingPeaksApi;
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AxiosResponse } from 'axios';
|
|
2
|
+
import { EventLogger } from 'gd-eventlog';
|
|
3
|
+
import { Observer } from '../../../../base/types/observer';
|
|
4
|
+
import { AppApiBase } from '../base';
|
|
5
|
+
import { TrainingPeaksAthlete, TrainingPeaksConfig, TrainingPeaksUploadProps, TrainingPeaksUploadResult, TrainingPeaksUploadStatus, TrainingPeaksWorkout } from './types';
|
|
6
|
+
export declare class TrainingPeaksApi extends AppApiBase {
|
|
7
|
+
protected config: TrainingPeaksConfig;
|
|
8
|
+
protected logger: EventLogger;
|
|
9
|
+
protected observer: Observer;
|
|
10
|
+
constructor();
|
|
11
|
+
protected getBaseUrl(): string;
|
|
12
|
+
protected getOauthBaseUrl(): string;
|
|
13
|
+
init(config: TrainingPeaksConfig): Observer;
|
|
14
|
+
update(config: Partial<TrainingPeaksConfig>): void;
|
|
15
|
+
isAuthenticated(): boolean;
|
|
16
|
+
getLoggedInAthlete(): Promise<TrainingPeaksAthlete>;
|
|
17
|
+
getCalendarWorkouts(opts?: {
|
|
18
|
+
oldest?: Date;
|
|
19
|
+
newest?: Date;
|
|
20
|
+
}): Promise<TrainingPeaksWorkout[]>;
|
|
21
|
+
getWorkoutFile(id: number, format?: string): Promise<string>;
|
|
22
|
+
upload(fileName: string, props?: TrainingPeaksUploadProps): Promise<TrainingPeaksUploadResult>;
|
|
23
|
+
getUploadStatus(trackingId: string): Promise<TrainingPeaksUploadStatus>;
|
|
24
|
+
protected verifyAuthentication(): Promise<void>;
|
|
25
|
+
protected verifyToken(): Promise<void>;
|
|
26
|
+
protected isTokenStillValid(): boolean;
|
|
27
|
+
protected refreshToken(): Promise<void>;
|
|
28
|
+
protected waitForUploadComplete(trackingId: string, externalId?: string): Promise<TrainingPeaksUploadResult>;
|
|
29
|
+
protected get(url: string, config?: object): Promise<AxiosResponse>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type TrainingPeaksConfig = {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken: string;
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
expiration?: Date;
|
|
7
|
+
};
|
|
8
|
+
export type TrainingPeaksRefreshTokenResponse = {
|
|
9
|
+
access_token: string;
|
|
10
|
+
token_type: string;
|
|
11
|
+
expires_in: number;
|
|
12
|
+
refresh_token: string;
|
|
13
|
+
scope: string;
|
|
14
|
+
};
|
|
15
|
+
export type TrainingPeaksAthlete = {
|
|
16
|
+
Id: number;
|
|
17
|
+
Username: string;
|
|
18
|
+
FirstName: string;
|
|
19
|
+
LastName: string;
|
|
20
|
+
Email: string;
|
|
21
|
+
};
|
|
22
|
+
export type TrainingPeaksWorkout = {
|
|
23
|
+
Id: number;
|
|
24
|
+
AthleteId: number;
|
|
25
|
+
Title: string;
|
|
26
|
+
WorkoutType: string;
|
|
27
|
+
WorkoutDay: string;
|
|
28
|
+
StartTimePlanned?: string;
|
|
29
|
+
TotalTimePlanned?: number;
|
|
30
|
+
TotalTime?: number;
|
|
31
|
+
DistancePlanned?: number;
|
|
32
|
+
Distance?: number;
|
|
33
|
+
TssPlanned?: number;
|
|
34
|
+
TssActual?: number;
|
|
35
|
+
IFPlanned?: number;
|
|
36
|
+
Completed: boolean;
|
|
37
|
+
LastModifiedDate: string;
|
|
38
|
+
WorkoutFileFormats?: string[];
|
|
39
|
+
Description?: string;
|
|
40
|
+
};
|
|
41
|
+
export type TrainingPeaksUploadStatus = {
|
|
42
|
+
Completed: boolean;
|
|
43
|
+
Status: string;
|
|
44
|
+
WorkoutIds: number[];
|
|
45
|
+
};
|
|
46
|
+
export type TrainingPeaksUploadProps = {
|
|
47
|
+
name?: string;
|
|
48
|
+
description?: string;
|
|
49
|
+
format?: string;
|
|
50
|
+
externalId?: string;
|
|
51
|
+
};
|
|
52
|
+
export type TrainingPeaksUploadResult = {
|
|
53
|
+
trainingPeaksId: number;
|
|
54
|
+
externalId?: string;
|
|
55
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TrainingPeaksApi, TrainingPeaksConfig } from '../base/api/trainingpeaks';
|
|
2
|
+
import { ConnectedAppService } from '../base/app';
|
|
3
|
+
import { TrainingPeaksCredentials } from './types';
|
|
4
|
+
export declare class TrainingPeaksAppConnection extends ConnectedAppService<TrainingPeaksCredentials> {
|
|
5
|
+
protected config: TrainingPeaksConfig;
|
|
6
|
+
protected api: TrainingPeaksApi;
|
|
7
|
+
protected tokenUpdateHandler: any;
|
|
8
|
+
constructor();
|
|
9
|
+
connect(credentials: TrainingPeaksCredentials): Promise<boolean>;
|
|
10
|
+
isConnecting(): boolean;
|
|
11
|
+
disconnect(): void;
|
|
12
|
+
isConnected(): boolean;
|
|
13
|
+
getCredentials(): TrainingPeaksCredentials;
|
|
14
|
+
getApi(): TrainingPeaksApi;
|
|
15
|
+
protected initAuth(): boolean;
|
|
16
|
+
protected updateConfig(config: TrainingPeaksConfig): void;
|
|
17
|
+
protected saveCredentials(config?: TrainingPeaksConfig): void;
|
|
18
|
+
protected buildConfigFromCredentials(credentials: TrainingPeaksCredentials): TrainingPeaksConfig;
|
|
19
|
+
protected initApi(config: TrainingPeaksConfig): void;
|
|
20
|
+
protected getSecret(key: string): string;
|
|
21
|
+
protected getSecretBindings(): import("../../api").ISecretBinding;
|
|
22
|
+
}
|
|
@@ -24,7 +24,8 @@ export interface GPXRidePageDisplayProps extends RidePageDisplayProps {
|
|
|
24
24
|
}
|
|
25
25
|
export type AnyRidePageDisplayProps = VideoRidePageDisplayProps | RidePageDisplayProps;
|
|
26
26
|
export interface RideMenuProps {
|
|
27
|
-
showResume
|
|
27
|
+
showResume?: boolean;
|
|
28
|
+
finished?: boolean;
|
|
28
29
|
}
|
|
29
30
|
interface RidePageCallbacks {
|
|
30
31
|
onMenuOpen(): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TrainingPeaksAppConnection } from '../../../../apps/trainingpeaks/TrainingPeaksAppConnection';
|
|
2
|
+
import { TrainingPeaksWorkout } from '../../../../apps/base/api/trainingpeaks/types';
|
|
3
|
+
import { IncyclistService } from '../../../../base/service';
|
|
4
|
+
import { Observer } from '../../../../base/types';
|
|
5
|
+
import { WorkoutCalendarEntry } from '../../types';
|
|
6
|
+
import { IWorkoutSyncProvider } from '../types';
|
|
7
|
+
export declare class TrainingPeaksCalendarSyncProvider extends IncyclistService implements IWorkoutSyncProvider {
|
|
8
|
+
protected lastSyncTS: number;
|
|
9
|
+
protected observer: Observer;
|
|
10
|
+
protected stopRequested: boolean;
|
|
11
|
+
protected workouts: WorkoutCalendarEntry[];
|
|
12
|
+
constructor();
|
|
13
|
+
sync(): Observer;
|
|
14
|
+
isConnected(): boolean;
|
|
15
|
+
reset(): void;
|
|
16
|
+
protected stopSync(): void;
|
|
17
|
+
protected loadWorkouts(): Promise<WorkoutCalendarEntry[]>;
|
|
18
|
+
protected parseWorkouts(workouts: TrainingPeaksWorkout[]): Promise<WorkoutCalendarEntry[]>;
|
|
19
|
+
protected getApi(): import("../../../../apps/base/api/trainingpeaks").TrainingPeaksApi;
|
|
20
|
+
protected getConnection(): TrainingPeaksAppConnection;
|
|
21
|
+
protected getAppsService(): import("../../../../apps").AppsService;
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incyclist-services",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.72",
|
|
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.22",
|
|
11
11
|
"promise.any": "^2.0.6",
|
|
12
12
|
"semver": "^7.7.4",
|
|
13
13
|
"tcx-builder": "^1.1.1",
|