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.
Files changed (33) hide show
  1. package/lib/cjs/devices/configuration/service.js +25 -2
  2. package/lib/cjs/devices/pairing/service.js +5 -4
  3. package/lib/cjs/routes/base/parsers/tacx/TacxParser.js +1 -4
  4. package/lib/cjs/routes/base/parsers/utils.js +13 -2
  5. package/lib/cjs/routes/download/service.js +4 -2
  6. package/lib/cjs/routes/list/cards/RouteCard.js +1 -1
  7. package/lib/cjs/routes/list/loaders/db.js +41 -6
  8. package/lib/cjs/routes/list/loaders/types.js +24 -1
  9. package/lib/cjs/routes/list/service.js +7 -0
  10. package/lib/cjs/workouts/ride/page/service.js +7 -3
  11. package/lib/cjs/workouts/ride/service.js +39 -24
  12. package/lib/esm/devices/configuration/service.js +25 -2
  13. package/lib/esm/devices/pairing/service.js +5 -4
  14. package/lib/esm/routes/base/parsers/tacx/TacxParser.js +2 -5
  15. package/lib/esm/routes/base/parsers/utils.js +10 -1
  16. package/lib/esm/routes/download/service.js +4 -2
  17. package/lib/esm/routes/list/cards/RouteCard.js +1 -1
  18. package/lib/esm/routes/list/loaders/db.js +36 -1
  19. package/lib/esm/routes/list/loaders/types.js +22 -0
  20. package/lib/esm/routes/list/service.js +7 -0
  21. package/lib/esm/workouts/ride/page/service.js +7 -3
  22. package/lib/esm/workouts/ride/service.js +39 -24
  23. package/lib/types/devices/configuration/service.d.ts +2 -0
  24. package/lib/types/devices/pairing/model.d.ts +2 -1
  25. package/lib/types/devices/pairing/service.d.ts +1 -1
  26. package/lib/types/routes/base/parsers/utils.d.ts +2 -0
  27. package/lib/types/routes/list/cards/RouteCard.d.ts +1 -1
  28. package/lib/types/routes/list/loaders/db.d.ts +2 -0
  29. package/lib/types/routes/list/loaders/types.d.ts +2 -0
  30. package/lib/types/workouts/ride/page/service.d.ts +2 -2
  31. package/lib/types/workouts/ride/service.d.ts +4 -3
  32. package/lib/types/workouts/ride/types.d.ts +4 -0
  33. 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
- this.settings.devices[settingIdx].settings = { ...this.settings.devices[settingIdx].settings, ...deviceSettings };
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
- if (filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath)) {
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
- this.logEvent({ message: 'download finished ', title, id, url });
221
- observer.emit('done', `video:///${file}`);
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.url) {
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 route_1 = require("../../base/utils/route");
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, route_1.validateDistance)(details.points);
264
- (0, route_1.validateSlopes)(details.points);
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, route_1.getTotalElevation)(details);
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
- (0, route_1.addDetails)(route, details);
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 ||= this.verifyVideoUrl(route);
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 text = title ? `${target} for ${duration} - ${title}` : `${target} for ${duration}`;
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
- let ch = ': ';
523
- if (segment?.text) {
524
- title += `${ch}${segment.text}`;
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
- title += `(${repeatCount}/${segment.repeat})`;
542
- }
543
- }
544
- return title;
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
- this.settings.devices[settingIdx].settings = { ...this.settings.devices[settingIdx].settings, ...deviceSettings };
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
- if (filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath)) {
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
- this.logEvent({ message: 'download finished ', title, id, url });
218
- observer.emit('done', `video:///${file}`);
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.url) {
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 ||= this.verifyVideoUrl(route);
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 text = title ? `${target} for ${duration} - ${title}` : `${target} for ${duration}`;
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
- let ch = ': ';
520
- if (segment?.text) {
521
- title += `${ch}${segment.text}`;
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
- title += `(${repeatCount}/${segment.repeat})`;
539
- }
540
- }
541
- return title;
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
- protected resetDownload(): Promise<void>;
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): void;
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): void;
37
- powerDown(delta: number): void;
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.86",
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.24",
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 --coverage -c jest.unit-config.cjs ",
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",