incyclist-services 1.7.53 → 1.7.55

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.
@@ -30,8 +30,8 @@ class IncyclistXMLParser extends xml_1.XMLParser {
30
30
  async loadPoints(context) {
31
31
  const { data, fileInfo, route } = context;
32
32
  const gpxFile = { ...fileInfo };
33
- const xmlName = fileInfo.name;
34
- const fileName = data['gpx-file-path'] ?? xmlName.replace('xml', 'gpx');
33
+ const xmlBase = fileInfo.base;
34
+ const fileName = data['gpx-file-path'] ?? xmlBase.replace('.xml', '.gpx');
35
35
  if (fileName.startsWith('file') || fileName.startsWith('/') || fileName.startsWith('\\') || fileName.startsWith('.')) {
36
36
  gpxFile.type = 'file';
37
37
  gpxFile.filename = fileName;
@@ -41,10 +41,10 @@ class IncyclistXMLParser extends xml_1.XMLParser {
41
41
  gpxFile.url = fileName;
42
42
  }
43
43
  else if (fileInfo.type === 'url') {
44
- gpxFile.url = gpxFile.url.replace(xmlName, fileName);
44
+ gpxFile.url = gpxFile.url.replace(xmlBase, fileName);
45
45
  }
46
46
  else {
47
- gpxFile.filename = gpxFile.filename.replace(xmlName, fileName);
47
+ gpxFile.filename = gpxFile.filename.replace(xmlBase, fileName);
48
48
  }
49
49
  try {
50
50
  let points;
@@ -73,7 +73,7 @@ class BinaryReader {
73
73
  exports.BinaryReader = BinaryReader;
74
74
  const getReferencedFileInfo = (info, referenced, scheme = 'file') => {
75
75
  if (info.type !== 'url') {
76
- return buildFromFile(info, referenced, scheme);
76
+ return buildFromFile(info, referenced);
77
77
  }
78
78
  if (referenced.url) {
79
79
  return referenced.url;
@@ -81,6 +81,9 @@ const getReferencedFileInfo = (info, referenced, scheme = 'file') => {
81
81
  if (!referenced.file) {
82
82
  return;
83
83
  }
84
+ if (referenced.file && info.filename?.startsWith('content://')) {
85
+ return `${info.dir}${info.delimiter}${referenced.file}`;
86
+ }
84
87
  const targetFileName = referenced.file;
85
88
  const regex = /([\\/])/g;
86
89
  if (targetFileName.startsWith('http://') || targetFileName.startsWith('https://')) {
@@ -97,9 +100,9 @@ const getReferencedFileInfo = (info, referenced, scheme = 'file') => {
97
100
  }
98
101
  };
99
102
  exports.getReferencedFileInfo = getReferencedFileInfo;
100
- const buildFromFile = (info, referenced, scheme = 'file') => {
103
+ const buildFromFile = (info, referenced) => {
101
104
  if (referenced.file) {
102
- const fileName = info.filename?.replace(info.name, referenced.file);
105
+ const fileName = info.filename?.replace(info.base, referenced.file);
103
106
  return `file:///${fileName}`;
104
107
  }
105
108
  return referenced.url;
@@ -107,9 +110,8 @@ const buildFromFile = (info, referenced, scheme = 'file') => {
107
110
  const buildAbsolutePathTarget = (fileName, info, scheme) => {
108
111
  const inputUrl = info.url;
109
112
  if (inputUrl.startsWith('incyclist:') || inputUrl.startsWith('file:')) {
110
- const target = {};
111
113
  const parts = inputUrl.split('://');
112
- const targetPath = parts[1].replace(info.name, fileName);
114
+ const targetPath = parts[1].replace(info.base, fileName);
113
115
  return `${scheme}://${targetPath}`;
114
116
  }
115
117
  };
@@ -95,7 +95,7 @@ let RouteLibraryScannerService = (() => {
95
95
  this.importProps = undefined;
96
96
  }
97
97
  getDisplayProps() {
98
- return this.importProps;
98
+ return { ...this.importProps };
99
99
  }
100
100
  importSingle(fileInfo) {
101
101
  const observer = new types_1.Observer();
@@ -125,8 +125,13 @@ let RouteLibraryScannerService = (() => {
125
125
  this.logError(err, 'scan', { uri: folderInfo.uri });
126
126
  observer.emit('error', err.message);
127
127
  });
128
- observer.on('scan-progress', (progress) => {
128
+ observer
129
+ .on('scan-progress', (progress) => {
129
130
  this.importProps.scanProgress = progress;
131
+ })
132
+ .on('scan-complete', () => {
133
+ console.log('# scan-complete');
134
+ this.importProps.phase = 'parsing';
130
135
  });
131
136
  return observer;
132
137
  }
@@ -143,9 +148,18 @@ let RouteLibraryScannerService = (() => {
143
148
  this.importProps.parseProgress = { parsed, total };
144
149
  });
145
150
  observer.on('parse-result', (route) => {
146
- this.importProps.routes.push(this.buildRouteDisplayItem(route));
151
+ const idx = this.importProps.routes.findIndex(r => r.id === route.controlFileUri);
152
+ if (idx !== -1) {
153
+ const observer = this.importProps.routes[idx].observer;
154
+ const displayProps = this.buildRouteDisplayItem(route, observer);
155
+ this.importProps.routes[idx] = displayProps;
156
+ if (observer) {
157
+ observer.emit('updated', displayProps);
158
+ }
159
+ }
147
160
  });
148
161
  observer.on('parse-complete', () => {
162
+ console.log('# parse-complete');
149
163
  this.importProps.phase = 'selecting';
150
164
  });
151
165
  return observer;
@@ -283,7 +297,7 @@ let RouteLibraryScannerService = (() => {
283
297
  const primaryFiles = files.filter((f) => {
284
298
  const name = typeof (f) === 'string' ? f : f.name;
285
299
  const ext = this.getExtension(name);
286
- return ext && parsers.isPrimaryExtension(ext);
300
+ return ext && ext !== 'gpx' && parsers.isPrimaryExtension(ext);
287
301
  }).map((f) => {
288
302
  if (typeof f === 'string') {
289
303
  return {
@@ -299,6 +313,7 @@ let RouteLibraryScannerService = (() => {
299
313
  if (!this.isCancelled) {
300
314
  const routeAnnouncement = await this.buildDiscoveredRoute(file, files, uri, folderName, parsers);
301
315
  discoveredCount.value++;
316
+ console.log('added', routeAnnouncement, this.scanResult.length);
302
317
  observer.emit('scan-result', routeAnnouncement);
303
318
  this.scanResult.push(routeAnnouncement);
304
319
  }
@@ -325,6 +340,7 @@ let RouteLibraryScannerService = (() => {
325
340
  return {
326
341
  folderUri,
327
342
  folderName,
343
+ files: folderFiles,
328
344
  controlFileUri: controlFile.uri,
329
345
  format: ext,
330
346
  scanError: skipReason
@@ -334,17 +350,29 @@ let RouteLibraryScannerService = (() => {
334
350
  const service = this.getRouteList();
335
351
  const targets = scannedRoutes.filter(r => !r.scanError);
336
352
  const total = targets.length;
353
+ targets.forEach(target => {
354
+ const file = this.buildFileInfo(target.controlFileUri, target.format);
355
+ this.importProps.routes.push({
356
+ format: target.format,
357
+ importable: false,
358
+ label: file.base,
359
+ id: target.controlFileUri,
360
+ alreadyImported: false,
361
+ observer: new types_1.Observer()
362
+ });
363
+ });
364
+ observer.emit('parse-start');
337
365
  for (let i = 0; i < targets.length; i++) {
338
366
  if (this.isCancelled)
339
367
  continue;
340
368
  const parsed = i + 1;
341
369
  const target = targets[i];
342
370
  observer.emit('parse-progress', { current: parsed, parsed, total, currentFolder: target.folderName });
343
- await this._parseTarget(target, service, observer);
371
+ await this._parseTarget(target, service, observer, i);
344
372
  }
345
373
  observer.emit('parse-complete');
346
374
  }
347
- async _parseTarget(target, service, observer) {
375
+ async _parseTarget(target, service, observer, idx) {
348
376
  if (service.existsBySourceUri(target.controlFileUri)) {
349
377
  observer.emit('parse-result', {
350
378
  alreadyImported: true,
@@ -365,7 +393,7 @@ let RouteLibraryScannerService = (() => {
365
393
  throw new Error(`Could not parse: [${err.message}]`);
366
394
  }
367
395
  if (result.data.hasVideo) {
368
- this.validateVideoUrl(result.details, target.folderUri);
396
+ this.validateVideoUrl(result.details, target.folderUri, target.files);
369
397
  }
370
398
  observer.emit('parse-result', {
371
399
  alreadyImported: false,
@@ -386,14 +414,14 @@ let RouteLibraryScannerService = (() => {
386
414
  });
387
415
  }
388
416
  }
389
- validateVideoUrl(routeDetail, folderUri) {
417
+ validateVideoUrl(routeDetail, folderUri, folderFiles) {
390
418
  if (this.isMobile()) {
391
419
  if (routeDetail.video.format === 'avi') {
392
420
  throw new Error('AVI video not supported');
393
421
  }
394
422
  }
395
423
  if (routeDetail.video.file) {
396
- routeDetail.video.file = this.resolveVideoUri(routeDetail.video.file, folderUri);
424
+ routeDetail.video.file = this.resolveVideoUri(routeDetail.video.file, folderUri, folderFiles);
397
425
  }
398
426
  }
399
427
  async _ingest(routes, observer) {
@@ -425,7 +453,7 @@ let RouteLibraryScannerService = (() => {
425
453
  const skipped = routes.length - target.length;
426
454
  observer.emit('ingest-complete', { imported: importedRoutes.length, skipped, errors, failedRoutes, importedRoutes });
427
455
  }
428
- resolveVideoUri(videoRef, folderUri) {
456
+ resolveVideoUri(videoRef, folderUri, folderFiles) {
429
457
  if (videoRef.startsWith('http://') || videoRef.startsWith('https://')) {
430
458
  return videoRef;
431
459
  }
@@ -434,10 +462,13 @@ let RouteLibraryScannerService = (() => {
434
462
  }
435
463
  if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
436
464
  if (this.isMobile())
437
- throw new Error('Absolute video path references are not supported during ingest');
465
+ throw new Error('Absolute video path references are not supported');
438
466
  return videoRef;
439
467
  }
440
- return `${folderUri}/${videoRef}`;
468
+ const match = folderFiles.find(f => f.name.toLowerCase() === videoRef.toLowerCase());
469
+ if (!match)
470
+ throw new Error(`Video file not found in folder: ${videoRef}`);
471
+ return match.uri;
441
472
  }
442
473
  async upsertImportHistory(folderInfo, routeCount) {
443
474
  try {
@@ -461,37 +492,37 @@ let RouteLibraryScannerService = (() => {
461
492
  }
462
493
  }
463
494
  buildFileInfo(uri, ext) {
464
- const lastSlash = Math.max(uri.lastIndexOf('/'), uri.lastIndexOf('\\'));
465
- const dir = uri.slice(0, lastSlash);
466
- const base = uri.slice(lastSlash + 1);
467
- const name = base.slice(0, base.length - ext.length - 1);
495
+ const { dir, base, name } = this.getBindings().path.parse(uri);
468
496
  return {
469
- type: 'url',
497
+ type: 'file',
470
498
  url: uri,
471
499
  filename: uri,
472
500
  base,
473
501
  name,
474
502
  dir,
475
503
  ext,
476
- delimiter: '/'
504
+ delimiter: uri.startsWith('content://') ? '%2F' : '/'
477
505
  };
478
506
  }
479
- buildRouteDisplayItem(parsed) {
480
- const { route, alreadyImported, parseError, format } = parsed;
507
+ buildRouteDisplayItem(parsed, observer) {
508
+ const { route, alreadyImported, parseError, format, controlFileUri } = parsed;
481
509
  const descr = route?.description ?? {};
482
510
  const [C, U] = this.getUnitConversionShortcuts();
483
511
  const distance = descr.distance === undefined ? undefined : {
484
512
  value: C(descr.distance, 'distance', { digits: 1 }),
485
513
  unit: U('distance')
486
514
  };
515
+ const path = this.getBindings().path;
516
+ const info = path.parse(controlFileUri);
487
517
  return {
488
- id: route.description.id,
518
+ id: route?.description?.id ?? info?.base,
489
519
  distance,
490
- label: route.title,
520
+ label: route?.title ?? info?.base,
491
521
  alreadyImported,
492
- importable: parseError != null,
522
+ importable: parseError == null,
493
523
  format,
494
- errorReason: parseError
524
+ errorReason: parseError,
525
+ observer: observer ?? new types_1.Observer()
495
526
  };
496
527
  }
497
528
  getCompanionExts(parsers, primaryExt) {
@@ -508,13 +539,6 @@ let RouteLibraryScannerService = (() => {
508
539
  const dot = filename.lastIndexOf('.');
509
540
  return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
510
541
  }
511
- containsAbsolutePath(content) {
512
- if (/[>"']\//m.test(content))
513
- return true;
514
- if (/[A-Za-z]:[/\\]/m.test(content))
515
- return true;
516
- return false;
517
- }
518
542
  isMobile() {
519
543
  return this.getBindings()?.appInfo?.getChannel() === 'mobile';
520
544
  }
@@ -206,6 +206,8 @@ let RoutesPageService = (() => {
206
206
  }
207
207
  }
208
208
  onImportClicked() {
209
+ if (this.showImportDialog)
210
+ return;
209
211
  try {
210
212
  this.showImportDialog = true;
211
213
  this.getRouteLibraryScanner().prepare();
@@ -217,7 +219,9 @@ let RoutesPageService = (() => {
217
219
  }
218
220
  importSingleRoute(fileInfo) {
219
221
  try {
220
- return this.getRouteLibraryScanner().importSingle(fileInfo);
222
+ const observer = this.getRouteLibraryScanner().importSingle(fileInfo);
223
+ this.updatePageDisplay();
224
+ return observer;
221
225
  }
222
226
  catch (err) {
223
227
  this.logError(err, 'importSingleRoute');
@@ -260,11 +264,11 @@ let RoutesPageService = (() => {
260
264
  }
261
265
  onImportClosed() {
262
266
  try {
267
+ this.showImportDialog = false;
263
268
  if (this.importObserver)
264
269
  this.importObserver.stop();
265
270
  this.getRouteLibraryScanner().done();
266
271
  this.importObserver = undefined;
267
- this.showImportDialog = false;
268
272
  this.getPageObserver().emit('import-closed');
269
273
  this.serviceState = this.getRouteList().search();
270
274
  this.updatePageDisplay();
@@ -27,8 +27,8 @@ export class IncyclistXMLParser extends XMLParser {
27
27
  async loadPoints(context) {
28
28
  const { data, fileInfo, route } = context;
29
29
  const gpxFile = { ...fileInfo };
30
- const xmlName = fileInfo.name;
31
- const fileName = data['gpx-file-path'] ?? xmlName.replace('xml', 'gpx');
30
+ const xmlBase = fileInfo.base;
31
+ const fileName = data['gpx-file-path'] ?? xmlBase.replace('.xml', '.gpx');
32
32
  if (fileName.startsWith('file') || fileName.startsWith('/') || fileName.startsWith('\\') || fileName.startsWith('.')) {
33
33
  gpxFile.type = 'file';
34
34
  gpxFile.filename = fileName;
@@ -38,10 +38,10 @@ export class IncyclistXMLParser extends XMLParser {
38
38
  gpxFile.url = fileName;
39
39
  }
40
40
  else if (fileInfo.type === 'url') {
41
- gpxFile.url = gpxFile.url.replace(xmlName, fileName);
41
+ gpxFile.url = gpxFile.url.replace(xmlBase, fileName);
42
42
  }
43
43
  else {
44
- gpxFile.filename = gpxFile.filename.replace(xmlName, fileName);
44
+ gpxFile.filename = gpxFile.filename.replace(xmlBase, fileName);
45
45
  }
46
46
  try {
47
47
  let points;
@@ -69,7 +69,7 @@ export class BinaryReader {
69
69
  }
70
70
  export const getReferencedFileInfo = (info, referenced, scheme = 'file') => {
71
71
  if (info.type !== 'url') {
72
- return buildFromFile(info, referenced, scheme);
72
+ return buildFromFile(info, referenced);
73
73
  }
74
74
  if (referenced.url) {
75
75
  return referenced.url;
@@ -77,6 +77,9 @@ export const getReferencedFileInfo = (info, referenced, scheme = 'file') => {
77
77
  if (!referenced.file) {
78
78
  return;
79
79
  }
80
+ if (referenced.file && info.filename?.startsWith('content://')) {
81
+ return `${info.dir}${info.delimiter}${referenced.file}`;
82
+ }
80
83
  const targetFileName = referenced.file;
81
84
  const regex = /([\\/])/g;
82
85
  if (targetFileName.startsWith('http://') || targetFileName.startsWith('https://')) {
@@ -92,9 +95,9 @@ export const getReferencedFileInfo = (info, referenced, scheme = 'file') => {
92
95
  return `${scheme}:///${targetFileName}`;
93
96
  }
94
97
  };
95
- const buildFromFile = (info, referenced, scheme = 'file') => {
98
+ const buildFromFile = (info, referenced) => {
96
99
  if (referenced.file) {
97
- const fileName = info.filename?.replace(info.name, referenced.file);
100
+ const fileName = info.filename?.replace(info.base, referenced.file);
98
101
  return `file:///${fileName}`;
99
102
  }
100
103
  return referenced.url;
@@ -102,9 +105,8 @@ const buildFromFile = (info, referenced, scheme = 'file') => {
102
105
  const buildAbsolutePathTarget = (fileName, info, scheme) => {
103
106
  const inputUrl = info.url;
104
107
  if (inputUrl.startsWith('incyclist:') || inputUrl.startsWith('file:')) {
105
- const target = {};
106
108
  const parts = inputUrl.split('://');
107
- const targetPath = parts[1].replace(info.name, fileName);
109
+ const targetPath = parts[1].replace(info.base, fileName);
108
110
  return `${scheme}://${targetPath}`;
109
111
  }
110
112
  };
@@ -92,7 +92,7 @@ let RouteLibraryScannerService = (() => {
92
92
  this.importProps = undefined;
93
93
  }
94
94
  getDisplayProps() {
95
- return this.importProps;
95
+ return { ...this.importProps };
96
96
  }
97
97
  importSingle(fileInfo) {
98
98
  const observer = new Observer();
@@ -122,8 +122,13 @@ let RouteLibraryScannerService = (() => {
122
122
  this.logError(err, 'scan', { uri: folderInfo.uri });
123
123
  observer.emit('error', err.message);
124
124
  });
125
- observer.on('scan-progress', (progress) => {
125
+ observer
126
+ .on('scan-progress', (progress) => {
126
127
  this.importProps.scanProgress = progress;
128
+ })
129
+ .on('scan-complete', () => {
130
+ console.log('# scan-complete');
131
+ this.importProps.phase = 'parsing';
127
132
  });
128
133
  return observer;
129
134
  }
@@ -140,9 +145,18 @@ let RouteLibraryScannerService = (() => {
140
145
  this.importProps.parseProgress = { parsed, total };
141
146
  });
142
147
  observer.on('parse-result', (route) => {
143
- this.importProps.routes.push(this.buildRouteDisplayItem(route));
148
+ const idx = this.importProps.routes.findIndex(r => r.id === route.controlFileUri);
149
+ if (idx !== -1) {
150
+ const observer = this.importProps.routes[idx].observer;
151
+ const displayProps = this.buildRouteDisplayItem(route, observer);
152
+ this.importProps.routes[idx] = displayProps;
153
+ if (observer) {
154
+ observer.emit('updated', displayProps);
155
+ }
156
+ }
144
157
  });
145
158
  observer.on('parse-complete', () => {
159
+ console.log('# parse-complete');
146
160
  this.importProps.phase = 'selecting';
147
161
  });
148
162
  return observer;
@@ -280,7 +294,7 @@ let RouteLibraryScannerService = (() => {
280
294
  const primaryFiles = files.filter((f) => {
281
295
  const name = typeof (f) === 'string' ? f : f.name;
282
296
  const ext = this.getExtension(name);
283
- return ext && parsers.isPrimaryExtension(ext);
297
+ return ext && ext !== 'gpx' && parsers.isPrimaryExtension(ext);
284
298
  }).map((f) => {
285
299
  if (typeof f === 'string') {
286
300
  return {
@@ -296,6 +310,7 @@ let RouteLibraryScannerService = (() => {
296
310
  if (!this.isCancelled) {
297
311
  const routeAnnouncement = await this.buildDiscoveredRoute(file, files, uri, folderName, parsers);
298
312
  discoveredCount.value++;
313
+ console.log('added', routeAnnouncement, this.scanResult.length);
299
314
  observer.emit('scan-result', routeAnnouncement);
300
315
  this.scanResult.push(routeAnnouncement);
301
316
  }
@@ -322,6 +337,7 @@ let RouteLibraryScannerService = (() => {
322
337
  return {
323
338
  folderUri,
324
339
  folderName,
340
+ files: folderFiles,
325
341
  controlFileUri: controlFile.uri,
326
342
  format: ext,
327
343
  scanError: skipReason
@@ -331,17 +347,29 @@ let RouteLibraryScannerService = (() => {
331
347
  const service = this.getRouteList();
332
348
  const targets = scannedRoutes.filter(r => !r.scanError);
333
349
  const total = targets.length;
350
+ targets.forEach(target => {
351
+ const file = this.buildFileInfo(target.controlFileUri, target.format);
352
+ this.importProps.routes.push({
353
+ format: target.format,
354
+ importable: false,
355
+ label: file.base,
356
+ id: target.controlFileUri,
357
+ alreadyImported: false,
358
+ observer: new Observer()
359
+ });
360
+ });
361
+ observer.emit('parse-start');
334
362
  for (let i = 0; i < targets.length; i++) {
335
363
  if (this.isCancelled)
336
364
  continue;
337
365
  const parsed = i + 1;
338
366
  const target = targets[i];
339
367
  observer.emit('parse-progress', { current: parsed, parsed, total, currentFolder: target.folderName });
340
- await this._parseTarget(target, service, observer);
368
+ await this._parseTarget(target, service, observer, i);
341
369
  }
342
370
  observer.emit('parse-complete');
343
371
  }
344
- async _parseTarget(target, service, observer) {
372
+ async _parseTarget(target, service, observer, idx) {
345
373
  if (service.existsBySourceUri(target.controlFileUri)) {
346
374
  observer.emit('parse-result', {
347
375
  alreadyImported: true,
@@ -362,7 +390,7 @@ let RouteLibraryScannerService = (() => {
362
390
  throw new Error(`Could not parse: [${err.message}]`);
363
391
  }
364
392
  if (result.data.hasVideo) {
365
- this.validateVideoUrl(result.details, target.folderUri);
393
+ this.validateVideoUrl(result.details, target.folderUri, target.files);
366
394
  }
367
395
  observer.emit('parse-result', {
368
396
  alreadyImported: false,
@@ -383,14 +411,14 @@ let RouteLibraryScannerService = (() => {
383
411
  });
384
412
  }
385
413
  }
386
- validateVideoUrl(routeDetail, folderUri) {
414
+ validateVideoUrl(routeDetail, folderUri, folderFiles) {
387
415
  if (this.isMobile()) {
388
416
  if (routeDetail.video.format === 'avi') {
389
417
  throw new Error('AVI video not supported');
390
418
  }
391
419
  }
392
420
  if (routeDetail.video.file) {
393
- routeDetail.video.file = this.resolveVideoUri(routeDetail.video.file, folderUri);
421
+ routeDetail.video.file = this.resolveVideoUri(routeDetail.video.file, folderUri, folderFiles);
394
422
  }
395
423
  }
396
424
  async _ingest(routes, observer) {
@@ -422,7 +450,7 @@ let RouteLibraryScannerService = (() => {
422
450
  const skipped = routes.length - target.length;
423
451
  observer.emit('ingest-complete', { imported: importedRoutes.length, skipped, errors, failedRoutes, importedRoutes });
424
452
  }
425
- resolveVideoUri(videoRef, folderUri) {
453
+ resolveVideoUri(videoRef, folderUri, folderFiles) {
426
454
  if (videoRef.startsWith('http://') || videoRef.startsWith('https://')) {
427
455
  return videoRef;
428
456
  }
@@ -431,10 +459,13 @@ let RouteLibraryScannerService = (() => {
431
459
  }
432
460
  if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
433
461
  if (this.isMobile())
434
- throw new Error('Absolute video path references are not supported during ingest');
462
+ throw new Error('Absolute video path references are not supported');
435
463
  return videoRef;
436
464
  }
437
- return `${folderUri}/${videoRef}`;
465
+ const match = folderFiles.find(f => f.name.toLowerCase() === videoRef.toLowerCase());
466
+ if (!match)
467
+ throw new Error(`Video file not found in folder: ${videoRef}`);
468
+ return match.uri;
438
469
  }
439
470
  async upsertImportHistory(folderInfo, routeCount) {
440
471
  try {
@@ -458,37 +489,37 @@ let RouteLibraryScannerService = (() => {
458
489
  }
459
490
  }
460
491
  buildFileInfo(uri, ext) {
461
- const lastSlash = Math.max(uri.lastIndexOf('/'), uri.lastIndexOf('\\'));
462
- const dir = uri.slice(0, lastSlash);
463
- const base = uri.slice(lastSlash + 1);
464
- const name = base.slice(0, base.length - ext.length - 1);
492
+ const { dir, base, name } = this.getBindings().path.parse(uri);
465
493
  return {
466
- type: 'url',
494
+ type: 'file',
467
495
  url: uri,
468
496
  filename: uri,
469
497
  base,
470
498
  name,
471
499
  dir,
472
500
  ext,
473
- delimiter: '/'
501
+ delimiter: uri.startsWith('content://') ? '%2F' : '/'
474
502
  };
475
503
  }
476
- buildRouteDisplayItem(parsed) {
477
- const { route, alreadyImported, parseError, format } = parsed;
504
+ buildRouteDisplayItem(parsed, observer) {
505
+ const { route, alreadyImported, parseError, format, controlFileUri } = parsed;
478
506
  const descr = route?.description ?? {};
479
507
  const [C, U] = this.getUnitConversionShortcuts();
480
508
  const distance = descr.distance === undefined ? undefined : {
481
509
  value: C(descr.distance, 'distance', { digits: 1 }),
482
510
  unit: U('distance')
483
511
  };
512
+ const path = this.getBindings().path;
513
+ const info = path.parse(controlFileUri);
484
514
  return {
485
- id: route.description.id,
515
+ id: route?.description?.id ?? info?.base,
486
516
  distance,
487
- label: route.title,
517
+ label: route?.title ?? info?.base,
488
518
  alreadyImported,
489
- importable: parseError != null,
519
+ importable: parseError == null,
490
520
  format,
491
- errorReason: parseError
521
+ errorReason: parseError,
522
+ observer: observer ?? new Observer()
492
523
  };
493
524
  }
494
525
  getCompanionExts(parsers, primaryExt) {
@@ -505,13 +536,6 @@ let RouteLibraryScannerService = (() => {
505
536
  const dot = filename.lastIndexOf('.');
506
537
  return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
507
538
  }
508
- containsAbsolutePath(content) {
509
- if (/[>"']\//m.test(content))
510
- return true;
511
- if (/[A-Za-z]:[/\\]/m.test(content))
512
- return true;
513
- return false;
514
- }
515
539
  isMobile() {
516
540
  return this.getBindings()?.appInfo?.getChannel() === 'mobile';
517
541
  }
@@ -203,6 +203,8 @@ let RoutesPageService = (() => {
203
203
  }
204
204
  }
205
205
  onImportClicked() {
206
+ if (this.showImportDialog)
207
+ return;
206
208
  try {
207
209
  this.showImportDialog = true;
208
210
  this.getRouteLibraryScanner().prepare();
@@ -214,7 +216,9 @@ let RoutesPageService = (() => {
214
216
  }
215
217
  importSingleRoute(fileInfo) {
216
218
  try {
217
- return this.getRouteLibraryScanner().importSingle(fileInfo);
219
+ const observer = this.getRouteLibraryScanner().importSingle(fileInfo);
220
+ this.updatePageDisplay();
221
+ return observer;
218
222
  }
219
223
  catch (err) {
220
224
  this.logError(err, 'importSingleRoute');
@@ -257,11 +261,11 @@ let RoutesPageService = (() => {
257
261
  }
258
262
  onImportClosed() {
259
263
  try {
264
+ this.showImportDialog = false;
260
265
  if (this.importObserver)
261
266
  this.importObserver.stop();
262
267
  this.getRouteLibraryScanner().done();
263
268
  this.importObserver = undefined;
264
- this.showImportDialog = false;
265
269
  this.getPageObserver().emit('import-closed');
266
270
  this.serviceState = this.getRouteList().search();
267
271
  this.updatePageDisplay();
@@ -32,7 +32,6 @@ export declare class RouteLibraryScannerService extends IncyclistService {
32
32
  private buildRouteDisplayItem;
33
33
  private getCompanionExts;
34
34
  private getExtension;
35
- private containsAbsolutePath;
36
35
  private isMobile;
37
36
  protected getRouteList(): import("../list/service").RouteListService;
38
37
  protected getBindings(): import("../../api").IncyclistBindings;
@@ -1,4 +1,6 @@
1
+ import { ReadDirResult } from "../../api";
1
2
  import { FormattedNumber } from "../../i18n";
3
+ import { IObserver } from "../../types";
2
4
  import { Route } from "../base/model/route";
3
5
  export interface FolderInfo {
4
6
  uri: string;
@@ -10,6 +12,7 @@ export interface ScannedRoute {
10
12
  folderName: string;
11
13
  format: RouteFormat;
12
14
  scanError?: string;
15
+ files: ReadDirResult[];
13
16
  }
14
17
  export type RouteFormat = string;
15
18
  export interface ParsedRoute {
@@ -19,6 +22,7 @@ export interface ParsedRoute {
19
22
  alreadyImported: boolean;
20
23
  parseError?: string;
21
24
  format: RouteFormat;
25
+ observer: IObserver;
22
26
  }
23
27
  export interface RouteDisplayItem {
24
28
  id: string;
@@ -28,6 +32,7 @@ export interface RouteDisplayItem {
28
32
  alreadyImported: boolean;
29
33
  importable: boolean;
30
34
  errorReason?: string;
35
+ observer: IObserver;
31
36
  }
32
37
  export interface ImportDisplayProps {
33
38
  phase: 'landing' | 'scanning' | 'parsing' | 'selecting' | 'ingesting' | 'complete' | 'result' | 'error';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incyclist-services",
3
- "version": "1.7.53",
3
+ "version": "1.7.55",
4
4
  "peerDependencies": {
5
5
  "gd-eventlog": "^0.1.27"
6
6
  },