incyclist-services 1.7.86 → 1.7.88
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/configuration/service.js +25 -2
- package/lib/cjs/devices/pairing/service.js +5 -4
- package/lib/cjs/routes/base/parsers/tacx/TacxParser.js +1 -4
- package/lib/cjs/routes/base/parsers/utils.js +13 -2
- package/lib/cjs/routes/download/service.js +4 -2
- package/lib/cjs/routes/list/cards/RouteCard.js +1 -1
- package/lib/cjs/routes/list/loaders/db.js +41 -6
- package/lib/cjs/routes/list/loaders/types.js +24 -1
- package/lib/cjs/routes/list/service.js +7 -0
- package/lib/cjs/workouts/ride/page/service.js +7 -3
- package/lib/cjs/workouts/ride/service.js +39 -24
- package/lib/esm/devices/configuration/service.js +25 -2
- package/lib/esm/devices/pairing/service.js +5 -4
- package/lib/esm/routes/base/parsers/tacx/TacxParser.js +2 -5
- package/lib/esm/routes/base/parsers/utils.js +10 -1
- package/lib/esm/routes/download/service.js +4 -2
- package/lib/esm/routes/list/cards/RouteCard.js +1 -1
- package/lib/esm/routes/list/loaders/db.js +36 -1
- package/lib/esm/routes/list/loaders/types.js +22 -0
- package/lib/esm/routes/list/service.js +7 -0
- package/lib/esm/workouts/ride/page/service.js +7 -3
- package/lib/esm/workouts/ride/service.js +39 -24
- package/lib/types/devices/configuration/service.d.ts +2 -0
- package/lib/types/devices/pairing/model.d.ts +2 -1
- package/lib/types/devices/pairing/service.d.ts +1 -1
- package/lib/types/routes/base/parsers/utils.d.ts +2 -0
- package/lib/types/routes/list/cards/RouteCard.d.ts +1 -1
- package/lib/types/routes/list/loaders/db.d.ts +2 -0
- package/lib/types/routes/list/loaders/types.d.ts +2 -0
- package/lib/types/workouts/ride/page/service.d.ts +2 -2
- package/lib/types/workouts/ride/service.d.ts +4 -3
- package/lib/types/workouts/ride/types.d.ts +4 -0
- package/package.json +4 -3
|
@@ -235,13 +235,18 @@ let DeviceConfigurationService = (() => {
|
|
|
235
235
|
}
|
|
236
236
|
add(deviceSettings, props) {
|
|
237
237
|
let udid = this.getUdid(deviceSettings);
|
|
238
|
+
udid ??= this.getUdidForWifiAddressChange(deviceSettings);
|
|
238
239
|
const { legacy = false } = props || {};
|
|
239
240
|
const deviceAlreadyExists = udid !== undefined;
|
|
240
241
|
let adapter;
|
|
241
242
|
const updateWifiSettings = (udid) => {
|
|
242
243
|
const settingIdx = this.settings.devices.findIndex(d => d.udid === udid);
|
|
243
244
|
if (settingIdx !== -1 && this.settings.devices[settingIdx]?.settings?.interface === 'wifi') {
|
|
244
|
-
|
|
245
|
+
const merged = { ...this.settings.devices[settingIdx].settings, ...deviceSettings };
|
|
246
|
+
this.settings.devices[settingIdx].settings = merged;
|
|
247
|
+
if (this.adapters[udid]) {
|
|
248
|
+
this.adapters[udid].settings = merged;
|
|
249
|
+
}
|
|
245
250
|
}
|
|
246
251
|
};
|
|
247
252
|
if (deviceAlreadyExists && this.adapters[udid]) {
|
|
@@ -255,7 +260,7 @@ let DeviceConfigurationService = (() => {
|
|
|
255
260
|
this.logEvent({ message: 'could not create adapter' });
|
|
256
261
|
return;
|
|
257
262
|
}
|
|
258
|
-
udid = this.settings.devices?.find(d => adapter.isEqual(d.settings))?.udid;
|
|
263
|
+
udid = this.settings.devices?.find(d => adapter.isEqual(d.settings))?.udid ?? this.getUdidForWifiAddressChange(deviceSettings);
|
|
259
264
|
if (udid) {
|
|
260
265
|
updateWifiSettings(udid);
|
|
261
266
|
}
|
|
@@ -338,6 +343,24 @@ let DeviceConfigurationService = (() => {
|
|
|
338
343
|
const udid = udids.find(key => this.adapters[key]?.isEqual(deviceSettings));
|
|
339
344
|
return udid;
|
|
340
345
|
}
|
|
346
|
+
getUdidForWifiAddressChange(deviceSettings) {
|
|
347
|
+
const settings = deviceSettings;
|
|
348
|
+
if (settings?.interface !== 'wifi' || !settings.name)
|
|
349
|
+
return undefined;
|
|
350
|
+
const dc = this.getDirectConnectInterface();
|
|
351
|
+
if (typeof dc?.hasNameCollision === 'function' && dc.hasNameCollision(settings.name))
|
|
352
|
+
return undefined;
|
|
353
|
+
return this.settings.devices?.find(d => {
|
|
354
|
+
const s = d.settings;
|
|
355
|
+
return s?.interface === 'wifi' && s.name === settings.name;
|
|
356
|
+
})?.udid;
|
|
357
|
+
}
|
|
358
|
+
isEqualDeviceSettings(a, b) {
|
|
359
|
+
if (!a || !b)
|
|
360
|
+
return false;
|
|
361
|
+
const adapter = this.getAdapterFromSetting(a);
|
|
362
|
+
return adapter?.isEqual(b) ?? false;
|
|
363
|
+
}
|
|
341
364
|
setDisplayName(deviceSettings, displayName) {
|
|
342
365
|
const udid = this.getUdid(deviceSettings);
|
|
343
366
|
if (!udid)
|
|
@@ -833,7 +833,7 @@ let DevicePairingService = (() => {
|
|
|
833
833
|
return;
|
|
834
834
|
}
|
|
835
835
|
const deletedCapabilities = (this.state.deleted || [])
|
|
836
|
-
.filter(e => e.udid === udid)
|
|
836
|
+
.filter(e => e.udid === udid || (e.settings && this.getDeviceConfiguration().isEqualDeviceSettings(e.settings, deviceSettings)))
|
|
837
837
|
.map(e => e.capability);
|
|
838
838
|
deletedCapabilities.forEach(capability => {
|
|
839
839
|
this.getDeviceConfiguration().delete(udid, capability, true);
|
|
@@ -1417,8 +1417,9 @@ let DevicePairingService = (() => {
|
|
|
1417
1417
|
c.value = undefined;
|
|
1418
1418
|
c.connectState = undefined;
|
|
1419
1419
|
}
|
|
1420
|
+
const settings = this.getDeviceAdapter(udid)?.getSettings?.();
|
|
1420
1421
|
this.getDeviceConfiguration().delete(udid, capability, true);
|
|
1421
|
-
this.addToDeletedList(capability, udid);
|
|
1422
|
+
this.addToDeletedList(capability, udid, settings);
|
|
1422
1423
|
this.emitStateChange({ capabilities: this.state.capabilities });
|
|
1423
1424
|
}
|
|
1424
1425
|
isScanning() {
|
|
@@ -1446,11 +1447,11 @@ let DevicePairingService = (() => {
|
|
|
1446
1447
|
numberOfSelectedCababilities(udid) {
|
|
1447
1448
|
return (this.state.capabilities || []).map(c => c.selected === udid ? 1 : 0).reduce((a, c) => a + c, 0);
|
|
1448
1449
|
}
|
|
1449
|
-
addToDeletedList(capability, udid) {
|
|
1450
|
+
addToDeletedList(capability, udid, settings) {
|
|
1450
1451
|
if (this.isOnDeletedList(capability, udid))
|
|
1451
1452
|
return;
|
|
1452
1453
|
const c = this.getCapability(capability);
|
|
1453
|
-
this.state.deleted.push({ capability: c.capability, udid });
|
|
1454
|
+
this.state.deleted.push({ capability: c.capability, udid, settings });
|
|
1454
1455
|
}
|
|
1455
1456
|
isOnDeletedList(c, udid) {
|
|
1456
1457
|
const capability = this.getCapability(c);
|
|
@@ -142,10 +142,7 @@ let TacxParser = (() => {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
buildVideoUrl(filePath) {
|
|
145
|
-
|
|
146
|
-
return `video:///${filePath}`;
|
|
147
|
-
}
|
|
148
|
-
return `video://${filePath}`;
|
|
145
|
+
return (0, utils_2.buildVideoUrl)(filePath);
|
|
149
146
|
}
|
|
150
147
|
async buildResult(context) {
|
|
151
148
|
if (!context.rlvData || !context.pgmfData) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getUtf8Data = exports.fixIncorrectFileInfo = exports.parseInformations = exports.safeEncode = exports.safeDecode = exports.getReferencedFileInfo = exports.BinaryReader = void 0;
|
|
3
|
+
exports.getUtf8Data = exports.fixIncorrectFileInfo = exports.parseInformations = exports.buildFileUrl = exports.safeEncode = exports.safeDecode = exports.buildVideoUrl = exports.getReferencedFileInfo = exports.BinaryReader = void 0;
|
|
4
4
|
const api_1 = require("../../../api");
|
|
5
5
|
class BinaryReader {
|
|
6
6
|
pos;
|
|
@@ -116,6 +116,16 @@ const isAbsolutePath = (path) => {
|
|
|
116
116
|
return true;
|
|
117
117
|
return false;
|
|
118
118
|
};
|
|
119
|
+
const buildVideoUrl = (filePath) => {
|
|
120
|
+
if (isAbsolutePath(filePath)) {
|
|
121
|
+
if (/^[A-Za-z]:/.test(filePath)) {
|
|
122
|
+
return `video:///${filePath}`;
|
|
123
|
+
}
|
|
124
|
+
return `video://${filePath}`;
|
|
125
|
+
}
|
|
126
|
+
return `video://${filePath}`;
|
|
127
|
+
};
|
|
128
|
+
exports.buildVideoUrl = buildVideoUrl;
|
|
119
129
|
const safeDecode = (path) => {
|
|
120
130
|
try {
|
|
121
131
|
return decodeURIComponent(path);
|
|
@@ -144,6 +154,7 @@ const buildFileUrl = (normalizedPath) => {
|
|
|
144
154
|
}
|
|
145
155
|
return `file://${encodeURI(normalizedPath)}`;
|
|
146
156
|
};
|
|
157
|
+
exports.buildFileUrl = buildFileUrl;
|
|
147
158
|
const buildUrlFromFile = (info, referenced) => {
|
|
148
159
|
if (referenced.url) {
|
|
149
160
|
return referenced.url;
|
|
@@ -159,7 +170,7 @@ const buildUrlFromFile = (info, referenced) => {
|
|
|
159
170
|
return handleFileUrlPath(fileName);
|
|
160
171
|
}
|
|
161
172
|
const normalizedPath = (0, exports.safeDecode)(fileName);
|
|
162
|
-
return buildFileUrl(normalizedPath);
|
|
173
|
+
return (0, exports.buildFileUrl)(normalizedPath);
|
|
163
174
|
};
|
|
164
175
|
const buildAbsolutePathTarget = (fileName, info, scheme) => {
|
|
165
176
|
const inputUrl = info.url;
|
|
@@ -40,6 +40,7 @@ const decorators_1 = require("../../base/decorators");
|
|
|
40
40
|
const service_1 = require("../../base/service");
|
|
41
41
|
const settings_1 = require("../../settings");
|
|
42
42
|
const utils_1 = require("../../utils");
|
|
43
|
+
const utils_2 = require("../base/parsers/utils");
|
|
43
44
|
const api_2 = require("../list/loaders/api");
|
|
44
45
|
const db_1 = require("../list/loaders/db");
|
|
45
46
|
const types_1 = require("./types");
|
|
@@ -217,8 +218,9 @@ let RouteDownloadService = (() => {
|
|
|
217
218
|
session.on('progress', onProgress);
|
|
218
219
|
session.on('error', onError);
|
|
219
220
|
session.once('done', () => {
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
const localUrl = (0, utils_2.buildVideoUrl)(file);
|
|
222
|
+
this.logEvent({ message: 'download finished ', title, id, url, localUrl });
|
|
223
|
+
observer.emit('done', localUrl);
|
|
222
224
|
});
|
|
223
225
|
session.start();
|
|
224
226
|
}
|
|
@@ -362,7 +362,7 @@ let RouteCard = (() => {
|
|
|
362
362
|
}
|
|
363
363
|
if (descr?.hasVideo && !descr.videoUrl) {
|
|
364
364
|
const details = this.getRouteData();
|
|
365
|
-
if (details.video
|
|
365
|
+
if (details.video?.url) {
|
|
366
366
|
canStart = false;
|
|
367
367
|
videoChecking = true;
|
|
368
368
|
videoMissing = this.isVideoMissing();
|
|
@@ -41,10 +41,12 @@ exports.useRoutesDbLoader = exports.RoutesDbLoader = void 0;
|
|
|
41
41
|
const api_1 = require("../../../api");
|
|
42
42
|
const types_1 = require("../../../base/types");
|
|
43
43
|
const observer_1 = require("../../../base/types/observer");
|
|
44
|
+
const route_1 = require("../../base/model/route");
|
|
44
45
|
const LegacyDB_1 = require("./LegacyDB");
|
|
46
|
+
const types_2 = require("./types");
|
|
45
47
|
const DBLoader_1 = require("./DBLoader");
|
|
46
48
|
const utils_1 = require("../../../utils");
|
|
47
|
-
const
|
|
49
|
+
const route_2 = require("../../base/utils/route");
|
|
48
50
|
const clone_1 = __importDefault(require("../../../utils/clone"));
|
|
49
51
|
let RoutesDbLoader = (() => {
|
|
50
52
|
let _classDecorators = [types_1.Singleton];
|
|
@@ -126,8 +128,34 @@ let RoutesDbLoader = (() => {
|
|
|
126
128
|
if (!descr)
|
|
127
129
|
return;
|
|
128
130
|
const details = await this.loadDetailRecord(descr);
|
|
131
|
+
if (!details)
|
|
132
|
+
return details;
|
|
133
|
+
await this.correctPersistedVideoUrl(descr, details);
|
|
129
134
|
return details;
|
|
130
135
|
}
|
|
136
|
+
async correctPersistedVideoUrl(descr, details) {
|
|
137
|
+
const corrected = (0, types_2.correctVideoUrl)(details.video?.url, this.isMobile());
|
|
138
|
+
if (!corrected)
|
|
139
|
+
return false;
|
|
140
|
+
details.video.url = corrected;
|
|
141
|
+
if (descr.videoUrl !== corrected) {
|
|
142
|
+
descr.videoUrl = corrected;
|
|
143
|
+
}
|
|
144
|
+
await this.writeDetailsRecord(descr, details);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
async writeDetailsRecord(descr, details) {
|
|
148
|
+
const repo = this.getDetailsRepo(new route_1.Route(descr));
|
|
149
|
+
if (!repo)
|
|
150
|
+
return;
|
|
151
|
+
const id = descr.legacyId || descr.id;
|
|
152
|
+
try {
|
|
153
|
+
await repo.write(id, details);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
this.logger.logEvent({ message: 'could not persist corrected video URL', id, error: err.message });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
131
159
|
async writeRepo() {
|
|
132
160
|
if (this.saveObserver)
|
|
133
161
|
await this.saveObserver.wait();
|
|
@@ -260,20 +288,27 @@ let RoutesDbLoader = (() => {
|
|
|
260
288
|
if (details.localizedTitle && typeof (details.localizedTitle) === 'string') {
|
|
261
289
|
details.localizedTitle = { en: details.localizedTitle };
|
|
262
290
|
}
|
|
263
|
-
(0,
|
|
264
|
-
(0,
|
|
291
|
+
(0, route_2.validateDistance)(details.points);
|
|
292
|
+
(0, route_2.validateSlopes)(details.points);
|
|
265
293
|
}
|
|
266
294
|
validateDescription(description, details) {
|
|
267
295
|
if (!description.elevation) {
|
|
268
|
-
description.elevation = (0,
|
|
296
|
+
description.elevation = (0, route_2.getTotalElevation)(details);
|
|
269
297
|
}
|
|
270
298
|
}
|
|
271
299
|
async loadDetails(route, alreadyAdded) {
|
|
272
300
|
const details = await this.loadDetailRecord(route);
|
|
273
301
|
let updated = false;
|
|
274
|
-
|
|
302
|
+
const verifyVideoUrl = (route) => {
|
|
303
|
+
const res = this.verifyVideoUrl(route);
|
|
304
|
+
if (res) {
|
|
305
|
+
this.logger.logEvent({ message: 'video URL updated', url: route?.details?.video?.url });
|
|
306
|
+
}
|
|
307
|
+
return res;
|
|
308
|
+
};
|
|
309
|
+
(0, route_2.addDetails)(route, details);
|
|
275
310
|
updated ||= this.verifyRouteHash(route);
|
|
276
|
-
updated ||=
|
|
311
|
+
updated ||= verifyVideoUrl(route);
|
|
277
312
|
if (alreadyAdded)
|
|
278
313
|
this.emitRouteUpdate(route);
|
|
279
314
|
this.emitRouteAdded(route);
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Loader = void 0;
|
|
3
|
+
exports.Loader = exports.correctVideoUrl = void 0;
|
|
4
|
+
const api_1 = require("../../../api");
|
|
4
5
|
const utils_1 = require("../../../utils");
|
|
5
6
|
const valid_1 = require("../../../utils/valid");
|
|
7
|
+
const utils_2 = require("../../base/parsers/utils");
|
|
6
8
|
const route_1 = require("../../base/utils/route");
|
|
9
|
+
const MALFORMED_VIDEO_URL = /^video:\/{4,}/;
|
|
10
|
+
const correctVideoUrl = (url, isMobile = false) => {
|
|
11
|
+
if (!url || !url.startsWith('video:'))
|
|
12
|
+
return undefined;
|
|
13
|
+
const malformed = MALFORMED_VIDEO_URL.test(url);
|
|
14
|
+
if (!malformed && !isMobile)
|
|
15
|
+
return undefined;
|
|
16
|
+
const rawPath = malformed
|
|
17
|
+
? url.replace(/^video:\/{3}/, '')
|
|
18
|
+
: url.replace(/^video:\/{2}/, '');
|
|
19
|
+
return isMobile ? (0, utils_2.buildFileUrl)(rawPath) : (0, utils_2.buildVideoUrl)(rawPath);
|
|
20
|
+
};
|
|
21
|
+
exports.correctVideoUrl = correctVideoUrl;
|
|
7
22
|
class Loader {
|
|
8
23
|
loadObserver;
|
|
9
24
|
isCompleted(route) {
|
|
@@ -15,6 +30,9 @@ class Loader {
|
|
|
15
30
|
}
|
|
16
31
|
return true;
|
|
17
32
|
}
|
|
33
|
+
isMobile() {
|
|
34
|
+
return (0, api_1.getBindings)()?.appInfo?.getChannel() === 'mobile';
|
|
35
|
+
}
|
|
18
36
|
verifyRouteHash(route) {
|
|
19
37
|
const { description, details } = route;
|
|
20
38
|
const prev = description.routeHash;
|
|
@@ -35,6 +53,11 @@ class Loader {
|
|
|
35
53
|
return;
|
|
36
54
|
if (descr.videoUrl) {
|
|
37
55
|
const details = route.details;
|
|
56
|
+
const corrected = (0, exports.correctVideoUrl)(details.video.url, this.isMobile());
|
|
57
|
+
if (corrected) {
|
|
58
|
+
details.video.url = corrected;
|
|
59
|
+
updated = true;
|
|
60
|
+
}
|
|
38
61
|
if (details.video.url.startsWith('video:') && !descr.isDownloaded && !descr.isLocal) {
|
|
39
62
|
descr.isDownloaded = true;
|
|
40
63
|
updated = true;
|
|
@@ -892,6 +892,13 @@ let RouteListService = (() => {
|
|
|
892
892
|
this.db.save(route, false);
|
|
893
893
|
});
|
|
894
894
|
}
|
|
895
|
+
if (route.description.hasVideo && route.description.isDownloaded) {
|
|
896
|
+
card.videoExists().then(exists => {
|
|
897
|
+
if (!exists) {
|
|
898
|
+
card.resetDownload();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
895
902
|
})
|
|
896
903
|
.catch(() => {
|
|
897
904
|
});
|
|
@@ -326,12 +326,13 @@ let WorkoutRidePageService = (() => {
|
|
|
326
326
|
adjustLoad(deltaPct) {
|
|
327
327
|
try {
|
|
328
328
|
if (deltaPct >= 0)
|
|
329
|
-
this.getWorkoutRide().powerUp(deltaPct);
|
|
329
|
+
return this.getWorkoutRide().powerUp(deltaPct);
|
|
330
330
|
else
|
|
331
|
-
this.getWorkoutRide().powerDown(-deltaPct);
|
|
331
|
+
return this.getWorkoutRide().powerDown(-deltaPct);
|
|
332
332
|
}
|
|
333
333
|
catch (err) {
|
|
334
334
|
this.logError(err, 'adjustLoad');
|
|
335
|
+
return undefined;
|
|
335
336
|
}
|
|
336
337
|
}
|
|
337
338
|
onRideStateUpdate(state) {
|
|
@@ -458,7 +459,10 @@ let WorkoutRidePageService = (() => {
|
|
|
458
459
|
return { text: title, mode: wo.mode ?? null };
|
|
459
460
|
const target = (0, graph_1.getStepTargetText)(limits.step ?? {}, wo.ftp);
|
|
460
461
|
const duration = (0, graph_1.getStepDuration)({ duration: limits.duration });
|
|
461
|
-
const
|
|
462
|
+
const base = `${target} for ${duration}`;
|
|
463
|
+
let text = base;
|
|
464
|
+
if (title && title !== base)
|
|
465
|
+
text = title.startsWith('(') ? `${base}${title}` : `${base} - ${title}`;
|
|
462
466
|
return { text, mode: wo.mode ?? null };
|
|
463
467
|
}
|
|
464
468
|
getStepAbsolutePower(step, ftp) {
|
|
@@ -35,6 +35,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
35
35
|
};
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
exports.getWorkoutRide = exports.useWorkoutRide = exports.WorkoutRide = void 0;
|
|
38
|
+
const api_1 = require("../../api");
|
|
38
39
|
const service_1 = require("../../base/service");
|
|
39
40
|
const types_1 = require("../../base/types");
|
|
40
41
|
const observer_1 = require("../../base/types/observer");
|
|
@@ -42,6 +43,7 @@ const devices_1 = require("../../devices");
|
|
|
42
43
|
const settings_1 = require("../../settings");
|
|
43
44
|
const utils_1 = require("../../utils");
|
|
44
45
|
const valid_1 = require("../../utils/valid");
|
|
46
|
+
const graph_1 = require("../base/graph");
|
|
45
47
|
const list_1 = require("../list");
|
|
46
48
|
const decorators_1 = require("../../base/decorators");
|
|
47
49
|
const DEFAULT_FTP = 200;
|
|
@@ -280,19 +282,23 @@ let WorkoutRide = (() => {
|
|
|
280
282
|
this.currentLimits.targetPower = Math.min(this.currentLimits.targetPower + deltaVal, this.currentLimits.maxPower);
|
|
281
283
|
this.logEvent({ message: 'workout target power adjusted', targetPower: this.currentLimits.targetPower });
|
|
282
284
|
this.emit('update', this.getDashboardDisplayProperties());
|
|
283
|
-
return;
|
|
285
|
+
return { type: 'targetPower', value: this.currentLimits.targetPower };
|
|
284
286
|
}
|
|
287
|
+
let adjustedFtp;
|
|
285
288
|
if (this.settings?.ftp) {
|
|
286
289
|
this.settings.ftp = this.settings.ftp * (1 + delta / 100);
|
|
287
290
|
this.workoutList.setStartSettings(this.settings);
|
|
288
291
|
this.logEvent({ message: 'workout FTP adjusted', ftp: this.settings.ftp });
|
|
292
|
+
adjustedFtp = Math.round(this.settings.ftp);
|
|
289
293
|
}
|
|
290
294
|
this.manualPowerOffset += delta;
|
|
291
295
|
this.setCurrentLimits();
|
|
292
296
|
this.emit('update', this.getDashboardDisplayProperties());
|
|
297
|
+
return adjustedFtp !== undefined ? { type: 'ftp', value: adjustedFtp } : undefined;
|
|
293
298
|
}
|
|
294
299
|
catch (err) {
|
|
295
300
|
this.logError(err, 'powerUp');
|
|
301
|
+
return undefined;
|
|
296
302
|
}
|
|
297
303
|
}
|
|
298
304
|
powerDown(delta) {
|
|
@@ -306,19 +312,23 @@ let WorkoutRide = (() => {
|
|
|
306
312
|
this.currentLimits.targetPower = Math.max(this.currentLimits.targetPower - deltaVal, this.currentLimits.minPower);
|
|
307
313
|
this.logEvent({ message: 'workout target power adjusted', targetPower: this.currentLimits.targetPower });
|
|
308
314
|
this.emit('update', this.getDashboardDisplayProperties());
|
|
309
|
-
return;
|
|
315
|
+
return { type: 'targetPower', value: this.currentLimits.targetPower };
|
|
310
316
|
}
|
|
317
|
+
let adjustedFtp;
|
|
311
318
|
if (this.settings?.ftp) {
|
|
312
319
|
this.settings.ftp = this.settings.ftp / (1 + delta / 100);
|
|
313
320
|
this.workoutList.setStartSettings(this.settings);
|
|
314
321
|
this.logEvent({ message: 'workout FTP adjusted', ftp: this.settings.ftp });
|
|
322
|
+
adjustedFtp = Math.round(this.settings.ftp);
|
|
315
323
|
}
|
|
316
324
|
this.manualPowerOffset -= delta;
|
|
317
325
|
this.setCurrentLimits();
|
|
318
326
|
this.emit('update', this.getDashboardDisplayProperties());
|
|
327
|
+
return adjustedFtp !== undefined ? { type: 'ftp', value: adjustedFtp } : undefined;
|
|
319
328
|
}
|
|
320
329
|
catch (err) {
|
|
321
330
|
this.logError(err, 'powerDown');
|
|
331
|
+
return undefined;
|
|
322
332
|
}
|
|
323
333
|
}
|
|
324
334
|
toggleCyclingMode() {
|
|
@@ -513,35 +523,40 @@ let WorkoutRide = (() => {
|
|
|
513
523
|
}
|
|
514
524
|
return { start, stop };
|
|
515
525
|
}
|
|
526
|
+
getBindings() {
|
|
527
|
+
return (0, api_1.getBindings)();
|
|
528
|
+
}
|
|
516
529
|
getStepTitle(time) {
|
|
517
530
|
if (!this.workout)
|
|
518
531
|
return;
|
|
519
|
-
let title = this.workout.name;
|
|
520
532
|
const limit = this.workout.getLimits(time, true);
|
|
521
533
|
const segment = this.workout.getSegment(time);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
ch = ' - ';
|
|
526
|
-
}
|
|
527
|
-
if (segment?.text && segment?.repeat > 0) {
|
|
528
|
-
const repeatTime = segment.duration / segment.repeat;
|
|
529
|
-
const repeatCount = Math.floor((time - segment.getStart()) / repeatTime) + 1;
|
|
530
|
-
title += `(${repeatCount}/${segment.repeat})`;
|
|
531
|
-
}
|
|
532
|
-
if (!limit) {
|
|
533
|
-
title += `${ch}free`;
|
|
534
|
-
}
|
|
535
|
-
else {
|
|
536
|
-
if (limit?.text)
|
|
537
|
-
title += `${ch}${limit.text}`;
|
|
538
|
-
if (limit?.text && segment && !segment.text && segment.repeat > 0) {
|
|
534
|
+
const isMobile = this.getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
535
|
+
const repeatSuffix = (segment?.repeat > 0)
|
|
536
|
+
? (() => {
|
|
539
537
|
const repeatTime = segment.duration / segment.repeat;
|
|
540
538
|
const repeatCount = Math.floor((time - segment.getStart()) / repeatTime) + 1;
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
|
|
539
|
+
return `(${repeatCount}/${segment.repeat})`;
|
|
540
|
+
})()
|
|
541
|
+
: '';
|
|
542
|
+
const compose = (body) => isMobile ? body : `${this.workout.name}: ${body}`;
|
|
543
|
+
if (!limit)
|
|
544
|
+
return compose('free');
|
|
545
|
+
const segmentName = segment?.text;
|
|
546
|
+
const stepName = limit?.text;
|
|
547
|
+
if (segmentName && stepName)
|
|
548
|
+
return compose(`${segmentName}${repeatSuffix} - ${stepName}`);
|
|
549
|
+
if (segmentName)
|
|
550
|
+
return compose(`${segmentName}${repeatSuffix}`);
|
|
551
|
+
if (stepName)
|
|
552
|
+
return compose(`${stepName}${repeatSuffix}`);
|
|
553
|
+
const rawStep = this.currentLimits?.step ?? limit;
|
|
554
|
+
const verbalDescription = `${(0, graph_1.getStepTargetText)(rawStep, this.settings?.ftp)} for ${(0, graph_1.getStepDuration)(rawStep)}`;
|
|
555
|
+
if (isMobile)
|
|
556
|
+
return repeatSuffix || verbalDescription;
|
|
557
|
+
if (!segment)
|
|
558
|
+
return this.workout.name;
|
|
559
|
+
return compose(`${verbalDescription}${repeatSuffix}`);
|
|
545
560
|
}
|
|
546
561
|
getPowerVal(power, key) {
|
|
547
562
|
if (power === undefined)
|
|
@@ -229,13 +229,18 @@ let DeviceConfigurationService = (() => {
|
|
|
229
229
|
}
|
|
230
230
|
add(deviceSettings, props) {
|
|
231
231
|
let udid = this.getUdid(deviceSettings);
|
|
232
|
+
udid ??= this.getUdidForWifiAddressChange(deviceSettings);
|
|
232
233
|
const { legacy = false } = props || {};
|
|
233
234
|
const deviceAlreadyExists = udid !== undefined;
|
|
234
235
|
let adapter;
|
|
235
236
|
const updateWifiSettings = (udid) => {
|
|
236
237
|
const settingIdx = this.settings.devices.findIndex(d => d.udid === udid);
|
|
237
238
|
if (settingIdx !== -1 && this.settings.devices[settingIdx]?.settings?.interface === 'wifi') {
|
|
238
|
-
|
|
239
|
+
const merged = { ...this.settings.devices[settingIdx].settings, ...deviceSettings };
|
|
240
|
+
this.settings.devices[settingIdx].settings = merged;
|
|
241
|
+
if (this.adapters[udid]) {
|
|
242
|
+
this.adapters[udid].settings = merged;
|
|
243
|
+
}
|
|
239
244
|
}
|
|
240
245
|
};
|
|
241
246
|
if (deviceAlreadyExists && this.adapters[udid]) {
|
|
@@ -249,7 +254,7 @@ let DeviceConfigurationService = (() => {
|
|
|
249
254
|
this.logEvent({ message: 'could not create adapter' });
|
|
250
255
|
return;
|
|
251
256
|
}
|
|
252
|
-
udid = this.settings.devices?.find(d => adapter.isEqual(d.settings))?.udid;
|
|
257
|
+
udid = this.settings.devices?.find(d => adapter.isEqual(d.settings))?.udid ?? this.getUdidForWifiAddressChange(deviceSettings);
|
|
253
258
|
if (udid) {
|
|
254
259
|
updateWifiSettings(udid);
|
|
255
260
|
}
|
|
@@ -332,6 +337,24 @@ let DeviceConfigurationService = (() => {
|
|
|
332
337
|
const udid = udids.find(key => this.adapters[key]?.isEqual(deviceSettings));
|
|
333
338
|
return udid;
|
|
334
339
|
}
|
|
340
|
+
getUdidForWifiAddressChange(deviceSettings) {
|
|
341
|
+
const settings = deviceSettings;
|
|
342
|
+
if (settings?.interface !== 'wifi' || !settings.name)
|
|
343
|
+
return undefined;
|
|
344
|
+
const dc = this.getDirectConnectInterface();
|
|
345
|
+
if (typeof dc?.hasNameCollision === 'function' && dc.hasNameCollision(settings.name))
|
|
346
|
+
return undefined;
|
|
347
|
+
return this.settings.devices?.find(d => {
|
|
348
|
+
const s = d.settings;
|
|
349
|
+
return s?.interface === 'wifi' && s.name === settings.name;
|
|
350
|
+
})?.udid;
|
|
351
|
+
}
|
|
352
|
+
isEqualDeviceSettings(a, b) {
|
|
353
|
+
if (!a || !b)
|
|
354
|
+
return false;
|
|
355
|
+
const adapter = this.getAdapterFromSetting(a);
|
|
356
|
+
return adapter?.isEqual(b) ?? false;
|
|
357
|
+
}
|
|
335
358
|
setDisplayName(deviceSettings, displayName) {
|
|
336
359
|
const udid = this.getUdid(deviceSettings);
|
|
337
360
|
if (!udid)
|
|
@@ -827,7 +827,7 @@ let DevicePairingService = (() => {
|
|
|
827
827
|
return;
|
|
828
828
|
}
|
|
829
829
|
const deletedCapabilities = (this.state.deleted || [])
|
|
830
|
-
.filter(e => e.udid === udid)
|
|
830
|
+
.filter(e => e.udid === udid || (e.settings && this.getDeviceConfiguration().isEqualDeviceSettings(e.settings, deviceSettings)))
|
|
831
831
|
.map(e => e.capability);
|
|
832
832
|
deletedCapabilities.forEach(capability => {
|
|
833
833
|
this.getDeviceConfiguration().delete(udid, capability, true);
|
|
@@ -1411,8 +1411,9 @@ let DevicePairingService = (() => {
|
|
|
1411
1411
|
c.value = undefined;
|
|
1412
1412
|
c.connectState = undefined;
|
|
1413
1413
|
}
|
|
1414
|
+
const settings = this.getDeviceAdapter(udid)?.getSettings?.();
|
|
1414
1415
|
this.getDeviceConfiguration().delete(udid, capability, true);
|
|
1415
|
-
this.addToDeletedList(capability, udid);
|
|
1416
|
+
this.addToDeletedList(capability, udid, settings);
|
|
1416
1417
|
this.emitStateChange({ capabilities: this.state.capabilities });
|
|
1417
1418
|
}
|
|
1418
1419
|
isScanning() {
|
|
@@ -1440,11 +1441,11 @@ let DevicePairingService = (() => {
|
|
|
1440
1441
|
numberOfSelectedCababilities(udid) {
|
|
1441
1442
|
return (this.state.capabilities || []).map(c => c.selected === udid ? 1 : 0).reduce((a, c) => a + c, 0);
|
|
1442
1443
|
}
|
|
1443
|
-
addToDeletedList(capability, udid) {
|
|
1444
|
+
addToDeletedList(capability, udid, settings) {
|
|
1444
1445
|
if (this.isOnDeletedList(capability, udid))
|
|
1445
1446
|
return;
|
|
1446
1447
|
const c = this.getCapability(capability);
|
|
1447
|
-
this.state.deleted.push({ capability: c.capability, udid });
|
|
1448
|
+
this.state.deleted.push({ capability: c.capability, udid, settings });
|
|
1448
1449
|
}
|
|
1449
1450
|
isOnDeletedList(c, udid) {
|
|
1450
1451
|
const capability = this.getCapability(c);
|
|
@@ -40,7 +40,7 @@ import { getRouteHash, validateRoute } from "../../utils/route";
|
|
|
40
40
|
import { PGMFFileReader } from "./pgmf";
|
|
41
41
|
import { RLVFileReader } from "./rlv";
|
|
42
42
|
import { TacxFileReader } from "./TacxReader";
|
|
43
|
-
import { fixIncorrectFileInfo } from "../utils";
|
|
43
|
+
import { buildVideoUrl, fixIncorrectFileInfo } from "../utils";
|
|
44
44
|
import { EventLogger } from "gd-eventlog";
|
|
45
45
|
import { Injectable } from "../../../../base/decorators";
|
|
46
46
|
let TacxParser = (() => {
|
|
@@ -136,10 +136,7 @@ let TacxParser = (() => {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
buildVideoUrl(filePath) {
|
|
139
|
-
|
|
140
|
-
return `video:///${filePath}`;
|
|
141
|
-
}
|
|
142
|
-
return `video://${filePath}`;
|
|
139
|
+
return buildVideoUrl(filePath);
|
|
143
140
|
}
|
|
144
141
|
async buildResult(context) {
|
|
145
142
|
if (!context.rlvData || !context.pgmfData) {
|
|
@@ -111,6 +111,15 @@ const isAbsolutePath = (path) => {
|
|
|
111
111
|
return true;
|
|
112
112
|
return false;
|
|
113
113
|
};
|
|
114
|
+
export const buildVideoUrl = (filePath) => {
|
|
115
|
+
if (isAbsolutePath(filePath)) {
|
|
116
|
+
if (/^[A-Za-z]:/.test(filePath)) {
|
|
117
|
+
return `video:///${filePath}`;
|
|
118
|
+
}
|
|
119
|
+
return `video://${filePath}`;
|
|
120
|
+
}
|
|
121
|
+
return `video://${filePath}`;
|
|
122
|
+
};
|
|
114
123
|
export const safeDecode = (path) => {
|
|
115
124
|
try {
|
|
116
125
|
return decodeURIComponent(path);
|
|
@@ -128,7 +137,7 @@ const handleFileUrlPath = (fileName) => {
|
|
|
128
137
|
const cleanPath = safeDecode(fileName.replace(/^file:\/\/\/?/, ''));
|
|
129
138
|
return `file://${encodeURI(cleanPath)}`;
|
|
130
139
|
};
|
|
131
|
-
const buildFileUrl = (normalizedPath) => {
|
|
140
|
+
export const buildFileUrl = (normalizedPath) => {
|
|
132
141
|
if (isAbsolutePath(normalizedPath)) {
|
|
133
142
|
if (/^[A-Za-z]:/.exec(normalizedPath)) {
|
|
134
143
|
return `file:///${encodeURI(normalizedPath)}`;
|
|
@@ -37,6 +37,7 @@ import { Injectable, Singleton } from "../../base/decorators";
|
|
|
37
37
|
import { IncyclistService } from "../../base/service";
|
|
38
38
|
import { useUserSettings } from "../../settings";
|
|
39
39
|
import { waitNextTick } from "../../utils";
|
|
40
|
+
import { buildVideoUrl } from "../base/parsers/utils";
|
|
40
41
|
import { RoutesApiLoader } from "../list/loaders/api";
|
|
41
42
|
import { RoutesDbLoader } from "../list/loaders/db";
|
|
42
43
|
import { DownloadObserver } from "./types";
|
|
@@ -214,8 +215,9 @@ let RouteDownloadService = (() => {
|
|
|
214
215
|
session.on('progress', onProgress);
|
|
215
216
|
session.on('error', onError);
|
|
216
217
|
session.once('done', () => {
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
const localUrl = buildVideoUrl(file);
|
|
219
|
+
this.logEvent({ message: 'download finished ', title, id, url, localUrl });
|
|
220
|
+
observer.emit('done', localUrl);
|
|
219
221
|
});
|
|
220
222
|
session.start();
|
|
221
223
|
}
|
|
@@ -359,7 +359,7 @@ let RouteCard = (() => {
|
|
|
359
359
|
}
|
|
360
360
|
if (descr?.hasVideo && !descr.videoUrl) {
|
|
361
361
|
const details = this.getRouteData();
|
|
362
|
-
if (details.video
|
|
362
|
+
if (details.video?.url) {
|
|
363
363
|
canStart = false;
|
|
364
364
|
videoChecking = true;
|
|
365
365
|
videoMissing = this.isVideoMissing();
|
|
@@ -35,7 +35,9 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
|
|
|
35
35
|
import { JsonRepository } from "../../../api";
|
|
36
36
|
import { Singleton } from "../../../base/types";
|
|
37
37
|
import { PromiseObserver } from "../../../base/types/observer";
|
|
38
|
+
import { Route } from "../../base/model/route";
|
|
38
39
|
import { RoutesLegacyDbLoader } from "./LegacyDB";
|
|
40
|
+
import { correctVideoUrl } from "./types";
|
|
39
41
|
import { DBLoader } from "./DBLoader";
|
|
40
42
|
import { waitNextTick } from "../../../utils";
|
|
41
43
|
import { addDetails, getTotalElevation, validateDistance, validateSlopes } from "../../base/utils/route";
|
|
@@ -120,8 +122,34 @@ let RoutesDbLoader = (() => {
|
|
|
120
122
|
if (!descr)
|
|
121
123
|
return;
|
|
122
124
|
const details = await this.loadDetailRecord(descr);
|
|
125
|
+
if (!details)
|
|
126
|
+
return details;
|
|
127
|
+
await this.correctPersistedVideoUrl(descr, details);
|
|
123
128
|
return details;
|
|
124
129
|
}
|
|
130
|
+
async correctPersistedVideoUrl(descr, details) {
|
|
131
|
+
const corrected = correctVideoUrl(details.video?.url, this.isMobile());
|
|
132
|
+
if (!corrected)
|
|
133
|
+
return false;
|
|
134
|
+
details.video.url = corrected;
|
|
135
|
+
if (descr.videoUrl !== corrected) {
|
|
136
|
+
descr.videoUrl = corrected;
|
|
137
|
+
}
|
|
138
|
+
await this.writeDetailsRecord(descr, details);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
async writeDetailsRecord(descr, details) {
|
|
142
|
+
const repo = this.getDetailsRepo(new Route(descr));
|
|
143
|
+
if (!repo)
|
|
144
|
+
return;
|
|
145
|
+
const id = descr.legacyId || descr.id;
|
|
146
|
+
try {
|
|
147
|
+
await repo.write(id, details);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
this.logger.logEvent({ message: 'could not persist corrected video URL', id, error: err.message });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
125
153
|
async writeRepo() {
|
|
126
154
|
if (this.saveObserver)
|
|
127
155
|
await this.saveObserver.wait();
|
|
@@ -265,9 +293,16 @@ let RoutesDbLoader = (() => {
|
|
|
265
293
|
async loadDetails(route, alreadyAdded) {
|
|
266
294
|
const details = await this.loadDetailRecord(route);
|
|
267
295
|
let updated = false;
|
|
296
|
+
const verifyVideoUrl = (route) => {
|
|
297
|
+
const res = this.verifyVideoUrl(route);
|
|
298
|
+
if (res) {
|
|
299
|
+
this.logger.logEvent({ message: 'video URL updated', url: route?.details?.video?.url });
|
|
300
|
+
}
|
|
301
|
+
return res;
|
|
302
|
+
};
|
|
268
303
|
addDetails(route, details);
|
|
269
304
|
updated ||= this.verifyRouteHash(route);
|
|
270
|
-
updated ||=
|
|
305
|
+
updated ||= verifyVideoUrl(route);
|
|
271
306
|
if (alreadyAdded)
|
|
272
307
|
this.emitRouteUpdate(route);
|
|
273
308
|
this.emitRouteAdded(route);
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
+
import { getBindings } from "../../../api";
|
|
1
2
|
import { waitNextTick } from "../../../utils";
|
|
2
3
|
import { valid } from "../../../utils/valid";
|
|
4
|
+
import { buildFileUrl, buildVideoUrl } from "../../base/parsers/utils";
|
|
3
5
|
import { getRouteHash } from "../../base/utils/route";
|
|
6
|
+
const MALFORMED_VIDEO_URL = /^video:\/{4,}/;
|
|
7
|
+
export const correctVideoUrl = (url, isMobile = false) => {
|
|
8
|
+
if (!url || !url.startsWith('video:'))
|
|
9
|
+
return undefined;
|
|
10
|
+
const malformed = MALFORMED_VIDEO_URL.test(url);
|
|
11
|
+
if (!malformed && !isMobile)
|
|
12
|
+
return undefined;
|
|
13
|
+
const rawPath = malformed
|
|
14
|
+
? url.replace(/^video:\/{3}/, '')
|
|
15
|
+
: url.replace(/^video:\/{2}/, '');
|
|
16
|
+
return isMobile ? buildFileUrl(rawPath) : buildVideoUrl(rawPath);
|
|
17
|
+
};
|
|
4
18
|
export class Loader {
|
|
5
19
|
loadObserver;
|
|
6
20
|
isCompleted(route) {
|
|
@@ -12,6 +26,9 @@ export class Loader {
|
|
|
12
26
|
}
|
|
13
27
|
return true;
|
|
14
28
|
}
|
|
29
|
+
isMobile() {
|
|
30
|
+
return getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
31
|
+
}
|
|
15
32
|
verifyRouteHash(route) {
|
|
16
33
|
const { description, details } = route;
|
|
17
34
|
const prev = description.routeHash;
|
|
@@ -32,6 +49,11 @@ export class Loader {
|
|
|
32
49
|
return;
|
|
33
50
|
if (descr.videoUrl) {
|
|
34
51
|
const details = route.details;
|
|
52
|
+
const corrected = correctVideoUrl(details.video.url, this.isMobile());
|
|
53
|
+
if (corrected) {
|
|
54
|
+
details.video.url = corrected;
|
|
55
|
+
updated = true;
|
|
56
|
+
}
|
|
35
57
|
if (details.video.url.startsWith('video:') && !descr.isDownloaded && !descr.isLocal) {
|
|
36
58
|
descr.isDownloaded = true;
|
|
37
59
|
updated = true;
|
|
@@ -886,6 +886,13 @@ let RouteListService = (() => {
|
|
|
886
886
|
this.db.save(route, false);
|
|
887
887
|
});
|
|
888
888
|
}
|
|
889
|
+
if (route.description.hasVideo && route.description.isDownloaded) {
|
|
890
|
+
card.videoExists().then(exists => {
|
|
891
|
+
if (!exists) {
|
|
892
|
+
card.resetDownload();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
889
896
|
})
|
|
890
897
|
.catch(() => {
|
|
891
898
|
});
|
|
@@ -323,12 +323,13 @@ let WorkoutRidePageService = (() => {
|
|
|
323
323
|
adjustLoad(deltaPct) {
|
|
324
324
|
try {
|
|
325
325
|
if (deltaPct >= 0)
|
|
326
|
-
this.getWorkoutRide().powerUp(deltaPct);
|
|
326
|
+
return this.getWorkoutRide().powerUp(deltaPct);
|
|
327
327
|
else
|
|
328
|
-
this.getWorkoutRide().powerDown(-deltaPct);
|
|
328
|
+
return this.getWorkoutRide().powerDown(-deltaPct);
|
|
329
329
|
}
|
|
330
330
|
catch (err) {
|
|
331
331
|
this.logError(err, 'adjustLoad');
|
|
332
|
+
return undefined;
|
|
332
333
|
}
|
|
333
334
|
}
|
|
334
335
|
onRideStateUpdate(state) {
|
|
@@ -455,7 +456,10 @@ let WorkoutRidePageService = (() => {
|
|
|
455
456
|
return { text: title, mode: wo.mode ?? null };
|
|
456
457
|
const target = getStepTargetText(limits.step ?? {}, wo.ftp);
|
|
457
458
|
const duration = getStepDuration({ duration: limits.duration });
|
|
458
|
-
const
|
|
459
|
+
const base = `${target} for ${duration}`;
|
|
460
|
+
let text = base;
|
|
461
|
+
if (title && title !== base)
|
|
462
|
+
text = title.startsWith('(') ? `${base}${title}` : `${base} - ${title}`;
|
|
459
463
|
return { text, mode: wo.mode ?? null };
|
|
460
464
|
}
|
|
461
465
|
getStepAbsolutePower(step, ftp) {
|
|
@@ -32,6 +32,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
32
32
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
33
|
done = true;
|
|
34
34
|
};
|
|
35
|
+
import { getBindings } from "../../api";
|
|
35
36
|
import { IncyclistService } from "../../base/service";
|
|
36
37
|
import { Singleton } from "../../base/types";
|
|
37
38
|
import { Observer } from "../../base/types/observer";
|
|
@@ -39,6 +40,7 @@ import { useDeviceRide } from "../../devices";
|
|
|
39
40
|
import { useUserSettings } from "../../settings";
|
|
40
41
|
import { waitNextTick } from "../../utils";
|
|
41
42
|
import { valid } from "../../utils/valid";
|
|
43
|
+
import { getStepDuration, getStepTargetText } from "../base/graph";
|
|
42
44
|
import { useWorkoutList } from "../list";
|
|
43
45
|
import { Injectable } from "../../base/decorators";
|
|
44
46
|
const DEFAULT_FTP = 200;
|
|
@@ -277,19 +279,23 @@ let WorkoutRide = (() => {
|
|
|
277
279
|
this.currentLimits.targetPower = Math.min(this.currentLimits.targetPower + deltaVal, this.currentLimits.maxPower);
|
|
278
280
|
this.logEvent({ message: 'workout target power adjusted', targetPower: this.currentLimits.targetPower });
|
|
279
281
|
this.emit('update', this.getDashboardDisplayProperties());
|
|
280
|
-
return;
|
|
282
|
+
return { type: 'targetPower', value: this.currentLimits.targetPower };
|
|
281
283
|
}
|
|
284
|
+
let adjustedFtp;
|
|
282
285
|
if (this.settings?.ftp) {
|
|
283
286
|
this.settings.ftp = this.settings.ftp * (1 + delta / 100);
|
|
284
287
|
this.workoutList.setStartSettings(this.settings);
|
|
285
288
|
this.logEvent({ message: 'workout FTP adjusted', ftp: this.settings.ftp });
|
|
289
|
+
adjustedFtp = Math.round(this.settings.ftp);
|
|
286
290
|
}
|
|
287
291
|
this.manualPowerOffset += delta;
|
|
288
292
|
this.setCurrentLimits();
|
|
289
293
|
this.emit('update', this.getDashboardDisplayProperties());
|
|
294
|
+
return adjustedFtp !== undefined ? { type: 'ftp', value: adjustedFtp } : undefined;
|
|
290
295
|
}
|
|
291
296
|
catch (err) {
|
|
292
297
|
this.logError(err, 'powerUp');
|
|
298
|
+
return undefined;
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
powerDown(delta) {
|
|
@@ -303,19 +309,23 @@ let WorkoutRide = (() => {
|
|
|
303
309
|
this.currentLimits.targetPower = Math.max(this.currentLimits.targetPower - deltaVal, this.currentLimits.minPower);
|
|
304
310
|
this.logEvent({ message: 'workout target power adjusted', targetPower: this.currentLimits.targetPower });
|
|
305
311
|
this.emit('update', this.getDashboardDisplayProperties());
|
|
306
|
-
return;
|
|
312
|
+
return { type: 'targetPower', value: this.currentLimits.targetPower };
|
|
307
313
|
}
|
|
314
|
+
let adjustedFtp;
|
|
308
315
|
if (this.settings?.ftp) {
|
|
309
316
|
this.settings.ftp = this.settings.ftp / (1 + delta / 100);
|
|
310
317
|
this.workoutList.setStartSettings(this.settings);
|
|
311
318
|
this.logEvent({ message: 'workout FTP adjusted', ftp: this.settings.ftp });
|
|
319
|
+
adjustedFtp = Math.round(this.settings.ftp);
|
|
312
320
|
}
|
|
313
321
|
this.manualPowerOffset -= delta;
|
|
314
322
|
this.setCurrentLimits();
|
|
315
323
|
this.emit('update', this.getDashboardDisplayProperties());
|
|
324
|
+
return adjustedFtp !== undefined ? { type: 'ftp', value: adjustedFtp } : undefined;
|
|
316
325
|
}
|
|
317
326
|
catch (err) {
|
|
318
327
|
this.logError(err, 'powerDown');
|
|
328
|
+
return undefined;
|
|
319
329
|
}
|
|
320
330
|
}
|
|
321
331
|
toggleCyclingMode() {
|
|
@@ -510,35 +520,40 @@ let WorkoutRide = (() => {
|
|
|
510
520
|
}
|
|
511
521
|
return { start, stop };
|
|
512
522
|
}
|
|
523
|
+
getBindings() {
|
|
524
|
+
return getBindings();
|
|
525
|
+
}
|
|
513
526
|
getStepTitle(time) {
|
|
514
527
|
if (!this.workout)
|
|
515
528
|
return;
|
|
516
|
-
let title = this.workout.name;
|
|
517
529
|
const limit = this.workout.getLimits(time, true);
|
|
518
530
|
const segment = this.workout.getSegment(time);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
ch = ' - ';
|
|
523
|
-
}
|
|
524
|
-
if (segment?.text && segment?.repeat > 0) {
|
|
525
|
-
const repeatTime = segment.duration / segment.repeat;
|
|
526
|
-
const repeatCount = Math.floor((time - segment.getStart()) / repeatTime) + 1;
|
|
527
|
-
title += `(${repeatCount}/${segment.repeat})`;
|
|
528
|
-
}
|
|
529
|
-
if (!limit) {
|
|
530
|
-
title += `${ch}free`;
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
if (limit?.text)
|
|
534
|
-
title += `${ch}${limit.text}`;
|
|
535
|
-
if (limit?.text && segment && !segment.text && segment.repeat > 0) {
|
|
531
|
+
const isMobile = this.getBindings()?.appInfo?.getChannel() === 'mobile';
|
|
532
|
+
const repeatSuffix = (segment?.repeat > 0)
|
|
533
|
+
? (() => {
|
|
536
534
|
const repeatTime = segment.duration / segment.repeat;
|
|
537
535
|
const repeatCount = Math.floor((time - segment.getStart()) / repeatTime) + 1;
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
536
|
+
return `(${repeatCount}/${segment.repeat})`;
|
|
537
|
+
})()
|
|
538
|
+
: '';
|
|
539
|
+
const compose = (body) => isMobile ? body : `${this.workout.name}: ${body}`;
|
|
540
|
+
if (!limit)
|
|
541
|
+
return compose('free');
|
|
542
|
+
const segmentName = segment?.text;
|
|
543
|
+
const stepName = limit?.text;
|
|
544
|
+
if (segmentName && stepName)
|
|
545
|
+
return compose(`${segmentName}${repeatSuffix} - ${stepName}`);
|
|
546
|
+
if (segmentName)
|
|
547
|
+
return compose(`${segmentName}${repeatSuffix}`);
|
|
548
|
+
if (stepName)
|
|
549
|
+
return compose(`${stepName}${repeatSuffix}`);
|
|
550
|
+
const rawStep = this.currentLimits?.step ?? limit;
|
|
551
|
+
const verbalDescription = `${getStepTargetText(rawStep, this.settings?.ftp)} for ${getStepDuration(rawStep)}`;
|
|
552
|
+
if (isMobile)
|
|
553
|
+
return repeatSuffix || verbalDescription;
|
|
554
|
+
if (!segment)
|
|
555
|
+
return this.workout.name;
|
|
556
|
+
return compose(`${verbalDescription}${repeatSuffix}`);
|
|
542
557
|
}
|
|
543
558
|
getPowerVal(power, key) {
|
|
544
559
|
if (power === undefined)
|
|
@@ -39,6 +39,8 @@ export declare class DeviceConfigurationService extends IncyclistService {
|
|
|
39
39
|
}): string;
|
|
40
40
|
delete(udid: string, capability?: ExtendedIncyclistCapability, forceSingle?: boolean): void;
|
|
41
41
|
getUdid(deviceSettings: IncyclistDeviceSettings): string;
|
|
42
|
+
protected getUdidForWifiAddressChange(deviceSettings: IncyclistDeviceSettings): string | undefined;
|
|
43
|
+
isEqualDeviceSettings(a: IncyclistDeviceSettings, b: IncyclistDeviceSettings): boolean;
|
|
42
44
|
setDisplayName(deviceSettings: IncyclistDeviceSettings, displayName?: string): void;
|
|
43
45
|
getAdapter(udid: string): IncyclistDeviceAdapter;
|
|
44
46
|
canStartRide(): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DeviceData, DeviceSettings, IncyclistCapability, IncyclistDeviceAdapter } from "incyclist-devices";
|
|
2
2
|
import { EnrichedInterfaceSetting } from "../access";
|
|
3
3
|
import { RideServiceCheckFilter } from "../ride";
|
|
4
|
-
import { AdapterInfo } from "../configuration";
|
|
4
|
+
import { AdapterInfo, IncyclistDeviceSettings } from "../configuration";
|
|
5
5
|
export interface DevicePairingData {
|
|
6
6
|
udid: string;
|
|
7
7
|
name: string;
|
|
@@ -41,6 +41,7 @@ export interface PairingState {
|
|
|
41
41
|
export interface DeleteListEntry {
|
|
42
42
|
capability: IncyclistCapability;
|
|
43
43
|
udid: string;
|
|
44
|
+
settings?: IncyclistDeviceSettings;
|
|
44
45
|
}
|
|
45
46
|
export interface InternalPairingState extends PairingState {
|
|
46
47
|
initialized: boolean;
|
|
@@ -136,7 +136,7 @@ export declare class DevicePairingService extends IncyclistService {
|
|
|
136
136
|
protected isPairingWaiting(): NodeJS.Timeout;
|
|
137
137
|
protected _stopDeviceSelection(changed: boolean): Promise<void>;
|
|
138
138
|
private numberOfSelectedCababilities;
|
|
139
|
-
protected addToDeletedList(capability: IncyclistCapability | CapabilityData, udid: string): void;
|
|
139
|
+
protected addToDeletedList(capability: IncyclistCapability | CapabilityData, udid: string, settings?: IncyclistDeviceSettings): void;
|
|
140
140
|
protected isOnDeletedList(c: IncyclistCapability | CapabilityData, udid: string): boolean;
|
|
141
141
|
mappedCapability(c: CapabilityInformation): CapabilityData;
|
|
142
142
|
protected mappedCapabilities(capabilities: DeviceConfigurationInfo): Array<CapabilityData>;
|
|
@@ -21,8 +21,10 @@ export declare const getReferencedFileInfo: (info: FileInfo, referenced: {
|
|
|
21
21
|
file?: string;
|
|
22
22
|
url?: string;
|
|
23
23
|
}, scheme?: string) => string;
|
|
24
|
+
export declare const buildVideoUrl: (filePath: string) => string;
|
|
24
25
|
export declare const safeDecode: (path: string) => string;
|
|
25
26
|
export declare const safeEncode: (path: string) => string;
|
|
27
|
+
export declare const buildFileUrl: (normalizedPath: string) => string;
|
|
26
28
|
export declare const parseInformations: (informations?: Array<JSONObject>) => Array<RouteInfoText>;
|
|
27
29
|
export declare const fixIncorrectFileInfo: (file: Partial<FileInfo>) => void;
|
|
28
30
|
export declare const getUtf8Data: (res: string | Buffer) => string;
|
|
@@ -107,7 +107,7 @@ export declare class RouteCard extends BaseCard implements Card<Route> {
|
|
|
107
107
|
protected deleteRouteUserSettings(): void;
|
|
108
108
|
protected deleteFromUIList(): void;
|
|
109
109
|
protected markDeleted(): Promise<void>;
|
|
110
|
-
|
|
110
|
+
resetDownload(): Promise<void>;
|
|
111
111
|
protected deleteRoute(): Promise<void>;
|
|
112
112
|
protected logError(err: Error, fn: string): void;
|
|
113
113
|
protected getSettings(): RouteSettings;
|
|
@@ -19,6 +19,8 @@ export declare class RoutesDbLoader extends DBLoader<RouteInfoDBEntry> {
|
|
|
19
19
|
delete(route: Route): Promise<void>;
|
|
20
20
|
getDescription(id: string): RouteInfo;
|
|
21
21
|
getDetails(id: string): Promise<RouteApiDetail>;
|
|
22
|
+
protected correctPersistedVideoUrl(descr: RouteInfo, details: RouteApiDetail): Promise<boolean>;
|
|
23
|
+
protected writeDetailsRecord(descr: RouteInfo, details: RouteApiDetail): Promise<void>;
|
|
22
24
|
protected writeRepo(): Promise<void>;
|
|
23
25
|
protected write(): void;
|
|
24
26
|
protected getLegacyLoader(): RoutesLegacyDbLoader;
|
|
@@ -2,6 +2,7 @@ import { Observer } from "../../../base/types/observer";
|
|
|
2
2
|
import { RouteApiDescription } from "../../base/api/types";
|
|
3
3
|
import { Route } from "../../base/model/route";
|
|
4
4
|
import { RouteInfo } from "../../base/types";
|
|
5
|
+
export declare const correctVideoUrl: (url?: string, isMobile?: boolean) => string | undefined;
|
|
5
6
|
export interface RouteInfoDBEntry extends RouteInfo {
|
|
6
7
|
pointsEncoded?: string;
|
|
7
8
|
}
|
|
@@ -21,6 +22,7 @@ export declare abstract class Loader<T extends MinimalDescription> {
|
|
|
21
22
|
abstract save(route: Route): Promise<void>;
|
|
22
23
|
protected abstract buildRouteInfo(descr: T): RouteInfo;
|
|
23
24
|
protected isCompleted(route: Route): boolean;
|
|
25
|
+
protected isMobile(): boolean;
|
|
24
26
|
protected verifyRouteHash(route: Route): boolean;
|
|
25
27
|
protected verifyVideoUrl(route: Route): boolean;
|
|
26
28
|
protected verifyCountry(route: Route): Promise<void>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IncyclistPageService } from "../../../base/pages";
|
|
2
2
|
import type { IObserver } from "../../../base/typedefs";
|
|
3
3
|
import type { CurrentRideState, RideType } from "../../../types";
|
|
4
|
-
import type { WorkoutDisplayProperties } from "../types";
|
|
4
|
+
import type { PowerAdjustmentResult, WorkoutDisplayProperties } from "../types";
|
|
5
5
|
import type { Workout } from "../../base/model";
|
|
6
6
|
import type { StepDefinition } from "../../base/model/types";
|
|
7
7
|
import type { IWorkoutRidePageService, WorkoutDashboardLine, WorkoutGestureHint, WorkoutGraphActuals, WorkoutGraphPlan, WorkoutRideMenuProps, WorkoutRidePageDisplayProps, WorkoutUpcomingSteps } from "./types";
|
|
@@ -37,7 +37,7 @@ export declare class WorkoutRidePageService extends IncyclistPageService impleme
|
|
|
37
37
|
dontShowAgain: boolean;
|
|
38
38
|
}): void;
|
|
39
39
|
onCancelStart(): void;
|
|
40
|
-
adjustLoad(deltaPct: number):
|
|
40
|
+
adjustLoad(deltaPct: number): PowerAdjustmentResult | undefined;
|
|
41
41
|
protected onRideStateUpdate(state: CurrentRideState): void;
|
|
42
42
|
protected onWorkoutUpdate(): void;
|
|
43
43
|
protected onWorkoutFinished(): void;
|
|
@@ -4,7 +4,7 @@ import { Workout } from "../base/model";
|
|
|
4
4
|
import { CurrentStep, PowerLimit, StepDefinition } from "../base/model/types";
|
|
5
5
|
import { WorkoutListService } from "../list";
|
|
6
6
|
import { WorkoutSettings } from "../list/cards/types";
|
|
7
|
-
import { ActiveWorkoutLimit, WorkoutDisplayProperties } from "./types";
|
|
7
|
+
import { ActiveWorkoutLimit, PowerAdjustmentResult, WorkoutDisplayProperties } from "./types";
|
|
8
8
|
export declare class WorkoutRide extends IncyclistService {
|
|
9
9
|
protected manualTimeOffset: number;
|
|
10
10
|
protected manualPowerOffset: number;
|
|
@@ -33,8 +33,8 @@ export declare class WorkoutRide extends IncyclistService {
|
|
|
33
33
|
}): void;
|
|
34
34
|
forward(): void;
|
|
35
35
|
backward(): void;
|
|
36
|
-
powerUp(delta: number):
|
|
37
|
-
powerDown(delta: number):
|
|
36
|
+
powerUp(delta: number): PowerAdjustmentResult | undefined;
|
|
37
|
+
powerDown(delta: number): PowerAdjustmentResult | undefined;
|
|
38
38
|
toggleCyclingMode(): void;
|
|
39
39
|
getDashboardDisplayProperties(): WorkoutDisplayProperties;
|
|
40
40
|
getCurrentLimits(): ActiveWorkoutLimit;
|
|
@@ -56,6 +56,7 @@ export declare class WorkoutRide extends IncyclistService {
|
|
|
56
56
|
start: any;
|
|
57
57
|
stop: any;
|
|
58
58
|
};
|
|
59
|
+
protected getBindings(): import("../../api").IncyclistBindings;
|
|
59
60
|
protected getStepTitle(time: number): string;
|
|
60
61
|
protected getPowerVal(power: PowerLimit, key: 'min' | 'max'): number;
|
|
61
62
|
protected getFtpFromUserSettings(): any;
|
|
@@ -15,6 +15,10 @@ export interface ActiveWorkoutLimit extends WorkoutRequest {
|
|
|
15
15
|
remaining: number;
|
|
16
16
|
step?: StepDefinition;
|
|
17
17
|
}
|
|
18
|
+
export interface PowerAdjustmentResult {
|
|
19
|
+
type: 'ftp' | 'targetPower';
|
|
20
|
+
value: number;
|
|
21
|
+
}
|
|
18
22
|
export interface WorkoutDisplayProperties {
|
|
19
23
|
workout?: Workout;
|
|
20
24
|
title?: string;
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "incyclist-services",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.88",
|
|
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.25",
|
|
11
11
|
"promise.any": "^2.0.6",
|
|
12
12
|
"semver": "^7.7.4",
|
|
13
13
|
"tcx-builder": "^1.1.1",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"test:all": "npx jest",
|
|
43
43
|
"test:e2e": "npx jest --coverage -c jest.e2e-config.cjs --coverageDirectory coverage.e2e",
|
|
44
44
|
"test:unit": "npx jest --coverage -c jest.unit-config.cjs ",
|
|
45
|
-
"test": "npx jest
|
|
45
|
+
"test": "npx jest -c jest.unit-config.cjs",
|
|
46
|
+
"coverage": "npx jest --coverage -c jest.unit-config.cjs",
|
|
46
47
|
"dev": "tsc -p tsconfig.esm.json --watch",
|
|
47
48
|
"docgen": "typedoc --options typedoc.json",
|
|
48
49
|
"docgen1": "typedoc --readme none --excludeProtected --excludePrivate --plugin typedoc-plugin-no-inherit --plugin typedoc-plugin-markdown src/index.ts",
|