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.
- package/lib/cjs/routes/base/parsers/incyclist.js +4 -4
- package/lib/cjs/routes/base/parsers/utils.js +7 -5
- package/lib/cjs/routes/library/service.js +55 -31
- package/lib/cjs/routes/page/service.js +6 -2
- package/lib/esm/routes/base/parsers/incyclist.js +4 -4
- package/lib/esm/routes/base/parsers/utils.js +7 -5
- package/lib/esm/routes/library/service.js +55 -31
- package/lib/esm/routes/page/service.js +6 -2
- package/lib/types/routes/library/service.d.ts +0 -1
- package/lib/types/routes/library/types.d.ts +5 -0
- package/package.json +1 -1
|
@@ -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
|
|
34
|
-
const fileName = data['gpx-file-path'] ??
|
|
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(
|
|
44
|
+
gpxFile.url = gpxFile.url.replace(xmlBase, fileName);
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
47
|
-
gpxFile.filename = gpxFile.filename.replace(
|
|
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
|
|
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
|
|
103
|
+
const buildFromFile = (info, referenced) => {
|
|
101
104
|
if (referenced.file) {
|
|
102
|
-
const fileName = info.filename?.replace(info.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
465
|
+
throw new Error('Absolute video path references are not supported');
|
|
438
466
|
return videoRef;
|
|
439
467
|
}
|
|
440
|
-
|
|
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
|
|
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: '
|
|
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
|
|
518
|
+
id: route?.description?.id ?? info?.base,
|
|
489
519
|
distance,
|
|
490
|
-
label: route
|
|
520
|
+
label: route?.title ?? info?.base,
|
|
491
521
|
alreadyImported,
|
|
492
|
-
importable: parseError
|
|
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
|
-
|
|
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
|
|
31
|
-
const fileName = data['gpx-file-path'] ??
|
|
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(
|
|
41
|
+
gpxFile.url = gpxFile.url.replace(xmlBase, fileName);
|
|
42
42
|
}
|
|
43
43
|
else {
|
|
44
|
-
gpxFile.filename = gpxFile.filename.replace(
|
|
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
|
|
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
|
|
98
|
+
const buildFromFile = (info, referenced) => {
|
|
96
99
|
if (referenced.file) {
|
|
97
|
-
const fileName = info.filename?.replace(info.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
462
|
+
throw new Error('Absolute video path references are not supported');
|
|
435
463
|
return videoRef;
|
|
436
464
|
}
|
|
437
|
-
|
|
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
|
|
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: '
|
|
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
|
|
515
|
+
id: route?.description?.id ?? info?.base,
|
|
486
516
|
distance,
|
|
487
|
-
label: route
|
|
517
|
+
label: route?.title ?? info?.base,
|
|
488
518
|
alreadyImported,
|
|
489
|
-
importable: parseError
|
|
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
|
-
|
|
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';
|