incyclist-services 1.7.52 → 1.7.54

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.
@@ -572,6 +572,8 @@ let DeviceRideService = (() => {
572
572
  if (startType === 'start') {
573
573
  this.initForStart(ai, startProps, route, startPos, realityFactor, rideMode);
574
574
  }
575
+ ai.adapter.resumeLogging();
576
+ this.resumeLogging();
575
577
  const sType = (ai.isControl) ? 'bike' : 'sensor';
576
578
  const logProps = {};
577
579
  logProps[sType] = ai.adapter.getUniqueName();
@@ -44,8 +44,10 @@ const types_1 = require("../../base/types");
44
44
  const parsers_1 = require("../base/parsers");
45
45
  const service_2 = require("../list/service");
46
46
  const utils_1 = require("../../utils");
47
- const VIDEO_EXTENSIONS = new Set(['mp4', 'mov', 'mkv', 'm4v', 'mpg', 'mpeg', 'wmv', 'avi']);
48
- const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']);
47
+ const db_1 = require("../list/loaders/db");
48
+ const route_1 = require("../base/model/route");
49
+ const sleep_1 = require("../../utils/sleep");
50
+ const i18n_1 = require("../../i18n");
49
51
  let RouteLibraryScannerService = (() => {
50
52
  let _classDecorators = [decorators_1.Singleton];
51
53
  let _classDescriptor;
@@ -55,53 +57,220 @@ let RouteLibraryScannerService = (() => {
55
57
  let _instanceExtraInitializers = [];
56
58
  let _getRouteList_decorators;
57
59
  let _getBindings_decorators;
60
+ let _getRoutesDBLoader_decorators;
61
+ let _getParsers_decorators;
62
+ let _getUnitConverter_decorators;
58
63
  var RouteLibraryScannerService = class extends _classSuper {
59
64
  static { _classThis = this; }
60
65
  static {
61
66
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
62
67
  _getRouteList_decorators = [decorators_1.Injectable];
63
68
  _getBindings_decorators = [decorators_1.Injectable];
69
+ _getRoutesDBLoader_decorators = [decorators_1.Injectable];
70
+ _getParsers_decorators = [decorators_1.Injectable];
71
+ _getUnitConverter_decorators = [decorators_1.Injectable];
64
72
  __esDecorate(this, null, _getRouteList_decorators, { kind: "method", name: "getRouteList", static: false, private: false, access: { has: obj => "getRouteList" in obj, get: obj => obj.getRouteList }, metadata: _metadata }, null, _instanceExtraInitializers);
65
73
  __esDecorate(this, null, _getBindings_decorators, { kind: "method", name: "getBindings", static: false, private: false, access: { has: obj => "getBindings" in obj, get: obj => obj.getBindings }, metadata: _metadata }, null, _instanceExtraInitializers);
74
+ __esDecorate(this, null, _getRoutesDBLoader_decorators, { kind: "method", name: "getRoutesDBLoader", static: false, private: false, access: { has: obj => "getRoutesDBLoader" in obj, get: obj => obj.getRoutesDBLoader }, metadata: _metadata }, null, _instanceExtraInitializers);
75
+ __esDecorate(this, null, _getParsers_decorators, { kind: "method", name: "getParsers", static: false, private: false, access: { has: obj => "getParsers" in obj, get: obj => obj.getParsers }, metadata: _metadata }, null, _instanceExtraInitializers);
76
+ __esDecorate(this, null, _getUnitConverter_decorators, { kind: "method", name: "getUnitConverter", static: false, private: false, access: { has: obj => "getUnitConverter" in obj, get: obj => obj.getUnitConverter }, metadata: _metadata }, null, _instanceExtraInitializers);
66
77
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
67
78
  RouteLibraryScannerService = _classThis = _classDescriptor.value;
68
79
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
69
80
  __runInitializers(_classThis, _classExtraInitializers);
70
81
  }
82
+ isCancelled = (__runInitializers(this, _instanceExtraInitializers), false);
83
+ scanResult = [];
84
+ importProps;
71
85
  constructor() {
72
86
  super('RouteLibraryScanner');
73
- __runInitializers(this, _instanceExtraInitializers);
87
+ }
88
+ prepare() {
89
+ this.importProps = {
90
+ phase: 'landing',
91
+ routes: []
92
+ };
93
+ }
94
+ done() {
95
+ this.importProps = undefined;
96
+ }
97
+ getDisplayProps() {
98
+ return this.importProps;
99
+ }
100
+ importSingle(fileInfo) {
101
+ const observer = new types_1.Observer();
102
+ this.isCancelled = false;
103
+ this.importRoute(fileInfo, observer).catch(err => {
104
+ this.logError(err, 'importSingle', { file: fileInfo?.filename });
105
+ observer.emit('error', err.message);
106
+ });
107
+ observer.on('success', (route) => {
108
+ this.importProps.phase = 'result';
109
+ this.importProps.resultSuccess = { routeName: route.title };
110
+ });
111
+ observer.on('error', (error) => {
112
+ this.importProps.phase = 'result';
113
+ this.importProps.error = error;
114
+ });
115
+ return observer;
74
116
  }
75
117
  scan(folderInfo) {
118
+ if (!this.importProps)
119
+ this.prepare();
120
+ this.isCancelled = false;
121
+ this.scanResult = [];
122
+ this.importProps.phase = 'scanning';
76
123
  const observer = new types_1.Observer();
77
124
  this._scan(folderInfo, observer).catch(err => {
78
125
  this.logError(err, 'scan', { uri: folderInfo.uri });
79
126
  observer.emit('error', err.message);
80
127
  });
128
+ observer.on('scan-progress', (progress) => {
129
+ this.importProps.scanProgress = progress;
130
+ });
81
131
  return observer;
82
132
  }
83
- ingest(routes, treeUri) {
133
+ parse(scannedRoutes) {
84
134
  const observer = new types_1.Observer();
85
- this._ingest(routes, treeUri, observer).catch(err => {
86
- this.logError(err, 'ingest', { treeUri });
135
+ if (!this.importProps)
136
+ this.prepare();
137
+ this._parse(scannedRoutes, observer).catch(err => {
138
+ this.logError(err, 'parse');
87
139
  observer.emit('error', err.message);
88
140
  });
141
+ observer.on('parse-progress', (progress) => {
142
+ const { parsed, total } = progress;
143
+ this.importProps.parseProgress = { parsed, total };
144
+ });
145
+ observer.on('parse-result', (route) => {
146
+ this.importProps.routes.push(this.buildRouteDisplayItem(route));
147
+ });
148
+ observer.on('parse-complete', () => {
149
+ this.importProps.phase = 'selecting';
150
+ });
89
151
  return observer;
90
152
  }
153
+ ingest(routes) {
154
+ const observer = new types_1.Observer();
155
+ if (!this.importProps)
156
+ this.prepare();
157
+ const list = this.getRouteList();
158
+ list.pauseListUpdates();
159
+ this.importProps.phase = 'ingesting';
160
+ this._ingest(routes, observer)
161
+ .catch(err => {
162
+ this.logError(err, 'ingest');
163
+ observer.emit('error', err.message);
164
+ })
165
+ .finally(() => {
166
+ list.resumeListUpdates();
167
+ list.emitLists('updated', { source: 'system' });
168
+ });
169
+ observer.on('ingest-progress', (progress) => {
170
+ const { current, total, currentName } = progress;
171
+ this.importProps.ingestProgress = { current, total, currentName };
172
+ });
173
+ observer.on('ingest-complete', (status) => {
174
+ this.importProps.phase = 'complete';
175
+ const { imported, skipped, errors, failedRoutes } = status;
176
+ this.importProps.completionSummary = { imported, skipped, errors, failedRoutes };
177
+ });
178
+ return observer;
179
+ }
180
+ cancel() {
181
+ this.isCancelled = true;
182
+ this.importProps.phase = 'landing';
183
+ }
184
+ async importRoute(fileInfo, observer) {
185
+ await (0, sleep_1.sleep)(0);
186
+ observer.emit('parsing');
187
+ this.importProps.phase = 'parsing';
188
+ if (fileInfo?.ext === 'gpx') {
189
+ return this.importSingleGpxRoute(fileInfo, observer);
190
+ }
191
+ else {
192
+ return this.importSingleVideoRoute(fileInfo, observer);
193
+ }
194
+ }
195
+ async importSingleGpxRoute(fileInfo, observer) {
196
+ const list = this.getRouteList();
197
+ const db = this.getRoutesDBLoader();
198
+ try {
199
+ const { data, details } = await parsers_1.RouteParser.parse(fileInfo);
200
+ const route = new route_1.Route(data, details);
201
+ route.description.tsImported = Date.now();
202
+ await db.save(route, true);
203
+ list.addRoute(route, 'user');
204
+ observer.emit('success', route);
205
+ }
206
+ catch (err) {
207
+ observer.emit('error', err.message);
208
+ }
209
+ }
210
+ async importSingleVideoRoute(fileInfo, observer) {
211
+ const parsers = this.getParsers();
212
+ const { dir, ext, delimiter } = fileInfo;
213
+ const folderUri = dir.endsWith(delimiter ?? '/') ? dir.slice(0, -delimiter.length) : dir;
214
+ try {
215
+ if (!parsers.isPrimaryExtension(ext)) {
216
+ observer.emit('error', 'not a route control file');
217
+ return;
218
+ }
219
+ const scanObserver = new types_1.Observer();
220
+ await this.scanFolder(folderUri, folderUri, scanObserver, parsers, { scannedFolders: 0 }, { value: 0 }, false);
221
+ const files = this.scanResult;
222
+ this.scanResult = [];
223
+ scanObserver.stop();
224
+ if (files[0].scanError) {
225
+ observer.emit('error', files[0].scanError);
226
+ return;
227
+ }
228
+ else {
229
+ const file = files.find(file => file.controlFileUri.includes(fileInfo.base));
230
+ const parseObserver = this.parse([file]);
231
+ parseObserver.on('parse-result', (result) => {
232
+ parseObserver.stop();
233
+ if (result.parseError) {
234
+ observer.emit('error', result.parseError);
235
+ return;
236
+ }
237
+ const ingest = this.ingest([result]);
238
+ let ingestError;
239
+ ingest.once('ingest-error', (_, reason) => {
240
+ ingest.stop();
241
+ observer.emit('error', reason);
242
+ ingestError = reason;
243
+ });
244
+ ingest.once('ingest-complete', (summary) => {
245
+ ingest.stop();
246
+ if (!ingestError && summary.imported > 0) {
247
+ observer.emit('success', summary.importedRoutes?.[0]?.title);
248
+ }
249
+ else if (!ingestError && summary.imported === 0) {
250
+ observer.emit('error', 'not imported');
251
+ }
252
+ });
253
+ });
254
+ }
255
+ }
256
+ catch (err) {
257
+ observer.emit('error', err.message);
258
+ }
259
+ }
91
260
  async _scan(folderInfo, observer) {
92
261
  await (0, utils_1.waitNextTick)();
93
- const parsers = (0, parsers_1.useParsers)();
262
+ const parsers = this.getParsers();
94
263
  const progress = { scannedFolders: 0 };
95
264
  const discoveredCount = { value: 0 };
96
265
  await this.scanFolder(folderInfo.uri, folderInfo.displayName, observer, parsers, progress, discoveredCount);
97
266
  await this.upsertImportHistory(folderInfo, discoveredCount.value);
98
- observer.emit('scan-complete');
267
+ observer.emit('scan-complete', this.scanResult);
99
268
  }
100
- async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount) {
269
+ async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount, recursive = true) {
101
270
  const fs = this.getBindings().fs;
102
271
  let entries;
103
272
  try {
104
- entries = await fs.readdir(uri, { extended: true });
273
+ entries = await fs.readdir(uri, { recursive: false, extended: true });
105
274
  }
106
275
  catch (err) {
107
276
  this.logError(err, 'scanFolder', { uri });
@@ -111,139 +280,153 @@ let RouteLibraryScannerService = (() => {
111
280
  observer.emit('scan-progress', { scannedFolders: progress.scannedFolders });
112
281
  const files = entries.filter(e => !e.isDirectory);
113
282
  const dirs = entries.filter(e => e.isDirectory);
114
- const primaryFiles = files.filter(f => {
115
- const ext = this.getExtension(f.name);
283
+ const primaryFiles = files.filter((f) => {
284
+ const name = typeof (f) === 'string' ? f : f.name;
285
+ const ext = this.getExtension(name);
116
286
  return ext && parsers.isPrimaryExtension(ext);
287
+ }).map((f) => {
288
+ if (typeof f === 'string') {
289
+ return {
290
+ name: f,
291
+ isDirectory: false,
292
+ uri: this.getBindings().path.join(uri, f)
293
+ };
294
+ }
295
+ else
296
+ return f;
117
297
  });
118
298
  for (const file of primaryFiles) {
119
- const route = await this.buildDiscoveredRoute(file, files, uri, folderName, parsers);
120
- discoveredCount.value++;
121
- observer.emit('discovered', route);
299
+ if (!this.isCancelled) {
300
+ const routeAnnouncement = await this.buildDiscoveredRoute(file, files, uri, folderName, parsers);
301
+ discoveredCount.value++;
302
+ observer.emit('scan-result', routeAnnouncement);
303
+ this.scanResult.push(routeAnnouncement);
304
+ }
122
305
  }
123
306
  for (const dir of dirs) {
124
- await this.scanFolder(dir.uri, dir.name, observer, parsers, progress, discoveredCount);
307
+ if (!this.isCancelled && recursive) {
308
+ await this.scanFolder(dir.uri, dir.name, observer, parsers, progress, discoveredCount);
309
+ }
125
310
  }
126
311
  }
127
312
  async buildDiscoveredRoute(controlFile, folderFiles, folderUri, folderName, parsers) {
128
313
  const ext = this.getExtension(controlFile.name);
129
314
  const baseName = controlFile.name.slice(0, controlFile.name.length - ext.length - 1);
130
- let importable = true;
131
315
  let skipReason;
132
316
  const companionExts = this.getCompanionExts(parsers, ext);
133
317
  for (const compExt of companionExts) {
134
318
  const hasCompanion = folderFiles.some(f => this.getExtension(f.name).toLowerCase() === compExt.toLowerCase() &&
135
319
  f.name.toLowerCase().startsWith(baseName.toLowerCase()));
136
320
  if (!hasCompanion) {
137
- importable = false;
138
321
  skipReason = `Missing companion file (.${compExt})`;
139
322
  break;
140
323
  }
141
324
  }
142
- let hasVideo = false;
143
- const hasThumbnail = folderFiles.some(f => IMAGE_EXTENSIONS.has(this.getExtension(f.name).toLowerCase()));
144
- if (importable) {
145
- const videoFiles = folderFiles.filter(f => VIDEO_EXTENSIONS.has(this.getExtension(f.name).toLowerCase()));
146
- const nonAviVideo = videoFiles.find(f => this.getExtension(f.name).toLowerCase() !== 'avi');
147
- if (videoFiles.length === 0) {
148
- importable = false;
149
- skipReason = 'No video file found in folder';
150
- }
151
- else if (nonAviVideo) {
152
- hasVideo = true;
153
- }
154
- else {
155
- importable = false;
156
- skipReason = 'Only AVI video format found; AVI is not supported';
157
- }
158
- }
159
- if (importable) {
160
- try {
161
- const fs = this.getBindings().fs;
162
- const content = await fs.readFile(controlFile.uri);
163
- const text = typeof content === 'string' ? content : content?.toString?.('utf8') ?? '';
164
- if (this.containsAbsolutePath(text.slice(0, 4096))) {
165
- importable = false;
166
- skipReason = 'Route references an absolute video path';
167
- }
168
- }
169
- catch {
170
- }
171
- }
172
- const alreadyImported = await this.getRouteList()
173
- .existsBySourceUri(controlFile.uri)
174
- .catch(() => false);
175
325
  return {
176
- id: (0, uuid_1.v4)(),
177
326
  folderUri,
178
327
  folderName,
328
+ files: folderFiles,
179
329
  controlFileUri: controlFile.uri,
180
330
  format: ext,
181
- hasVideo,
182
- hasThumbnail,
183
- alreadyImported,
184
- importable,
185
- skipReason
331
+ scanError: skipReason
186
332
  };
187
333
  }
188
- async _ingest(routes, treeUri, observer) {
334
+ async _parse(scannedRoutes, observer) {
335
+ const service = this.getRouteList();
336
+ const targets = scannedRoutes.filter(r => !r.scanError);
337
+ const total = targets.length;
338
+ for (let i = 0; i < targets.length; i++) {
339
+ if (this.isCancelled)
340
+ continue;
341
+ const parsed = i + 1;
342
+ const target = targets[i];
343
+ observer.emit('parse-progress', { current: parsed, parsed, total, currentFolder: target.folderName });
344
+ await this._parseTarget(target, service, observer);
345
+ }
346
+ observer.emit('parse-complete');
347
+ }
348
+ async _parseTarget(target, service, observer) {
349
+ if (service.existsBySourceUri(target.controlFileUri)) {
350
+ observer.emit('parse-result', {
351
+ alreadyImported: true,
352
+ route: service.getBySourceUri(target.controlFileUri),
353
+ folderUri: target.folderUri,
354
+ controlFileUri: target.controlFileUri,
355
+ format: target.format
356
+ });
357
+ return;
358
+ }
359
+ let result;
360
+ const file = this.buildFileInfo(target.controlFileUri, target.format);
361
+ try {
362
+ try {
363
+ result = await parsers_1.RouteParser.parse(file);
364
+ }
365
+ catch (err) {
366
+ throw new Error(`Could not parse: [${err.message}]`);
367
+ }
368
+ if (result.data.hasVideo) {
369
+ this.validateVideoUrl(result.details, target.folderUri, target.files);
370
+ }
371
+ observer.emit('parse-result', {
372
+ alreadyImported: false,
373
+ route: new route_1.Route(result.data, result.details),
374
+ folderUri: target.folderUri,
375
+ controlFileUri: target.controlFileUri,
376
+ format: target.format
377
+ });
378
+ }
379
+ catch (err) {
380
+ observer.emit('parse-result', {
381
+ alreadyImported: false,
382
+ route: result ? new route_1.Route(result.data, result.details) : undefined,
383
+ folderUri: target.folderUri,
384
+ controlFileUri: target.controlFileUri,
385
+ format: target.format,
386
+ parseError: err?.message ?? String(err)
387
+ });
388
+ }
389
+ }
390
+ validateVideoUrl(routeDetail, folderUri, folderFiles) {
391
+ if (this.isMobile()) {
392
+ if (routeDetail.video.format === 'avi') {
393
+ throw new Error('AVI video not supported');
394
+ }
395
+ }
396
+ if (routeDetail.video.file) {
397
+ routeDetail.video.file = this.resolveVideoUri(routeDetail.video.file, folderUri, folderFiles);
398
+ }
399
+ }
400
+ async _ingest(routes, observer) {
189
401
  await (0, utils_1.waitNextTick)();
190
- const importable = routes.filter(r => r.importable && !r.alreadyImported);
191
- const total = importable.length;
192
- let imported = 0;
402
+ const service = this.getRouteList();
403
+ const db = this.getRoutesDBLoader();
404
+ const target = routes.filter(r => !r.alreadyImported && !r.parseError);
405
+ const total = target.length;
193
406
  let errors = 0;
194
407
  const failedRoutes = [];
195
- for (let i = 0; i < importable.length; i++) {
196
- const route = importable[i];
197
- observer.emit('ingest-progress', { current: i + 1, total, currentName: route.folderName });
408
+ const importedRoutes = [];
409
+ for (let i = 0; i < target.length; i++) {
410
+ if (this.isCancelled)
411
+ continue;
412
+ const { route } = target[i] ?? {};
198
413
  try {
199
- await this.ingestRoute(route, treeUri);
200
- imported++;
414
+ observer.emit('ingest-progress', { current: i + 1, total, currentName: route.title });
415
+ await db.save(route, true);
416
+ service.addRoute(route, 'user');
417
+ importedRoutes.push(route);
201
418
  }
202
419
  catch (err) {
203
420
  const reason = err?.message ?? String(err);
204
421
  errors++;
205
- failedRoutes.push({ name: route.folderName, reason });
206
- observer.emit('ingest-error', { name: route.folderName, reason });
422
+ failedRoutes.push({ name: route.title, reason });
423
+ observer.emit('ingest-error', { name: route.title, reason });
207
424
  }
208
425
  }
209
- const skipped = routes.length - importable.length;
210
- observer.emit('ingest-complete', { imported, skipped, errors, failedRoutes });
211
- }
212
- async ingestRoute(route, treeUri) {
213
- const { format, controlFileUri, folderUri } = route;
214
- const fileInfo = this.buildFileInfo(controlFileUri, format);
215
- const { data } = await parsers_1.RouteParser.parse(fileInfo);
216
- let thumbnailPath;
217
- if (route.hasThumbnail) {
218
- thumbnailPath = await this.copyThumbnail(route, folderUri).catch(() => undefined);
219
- }
220
- const videoUri = data.videoUrl ? this.resolveVideoUri(data.videoUrl, folderUri) : undefined;
221
- const record = {
222
- id: data.id ?? (0, uuid_1.v4)(),
223
- name: data.title ?? route.folderName,
224
- format,
225
- thumbnailPath,
226
- videoUri,
227
- sourceTreeUri: treeUri
228
- };
229
- await this.getRouteList().addRoute(record);
230
- }
231
- async copyThumbnail(route, folderUri) {
232
- const { fs, appInfo } = this.getBindings();
233
- if (!fs || !appInfo)
234
- return undefined;
235
- const entries = await fs.readdir(folderUri, { extended: true });
236
- const thumbnailFile = entries.find(e => !e.isDirectory && IMAGE_EXTENSIONS.has(this.getExtension(e.name).toLowerCase()));
237
- if (!thumbnailFile)
238
- return undefined;
239
- const destDir = `${appInfo.getAppDir()}/thumbnails`;
240
- await fs.ensureDir(destDir);
241
- const srcContent = await fs.readFile(thumbnailFile.uri);
242
- const destPath = `${destDir}/${route.id}.${this.getExtension(thumbnailFile.name)}`;
243
- await fs.writeFile(destPath, srcContent);
244
- return destPath;
245
- }
246
- resolveVideoUri(videoRef, folderUri) {
426
+ const skipped = routes.length - target.length;
427
+ observer.emit('ingest-complete', { imported: importedRoutes.length, skipped, errors, failedRoutes, importedRoutes });
428
+ }
429
+ resolveVideoUri(videoRef, folderUri, folderFiles) {
247
430
  if (videoRef.startsWith('http://') || videoRef.startsWith('https://')) {
248
431
  return videoRef;
249
432
  }
@@ -251,9 +434,14 @@ let RouteLibraryScannerService = (() => {
251
434
  return videoRef;
252
435
  }
253
436
  if (videoRef.startsWith('/') || /^[A-Za-z]:[/\\]/.test(videoRef)) {
254
- throw new Error('Absolute video path references are not supported during ingest');
437
+ if (this.isMobile())
438
+ throw new Error('Absolute video path references are not supported');
439
+ return videoRef;
255
440
  }
256
- return `${folderUri}/${videoRef}`;
441
+ const match = folderFiles.find(f => f.name.toLowerCase() === videoRef.toLowerCase());
442
+ if (!match)
443
+ throw new Error(`Video file not found in folder: ${videoRef}`);
444
+ return match.uri;
257
445
  }
258
446
  async upsertImportHistory(folderInfo, routeCount) {
259
447
  try {
@@ -292,6 +480,24 @@ let RouteLibraryScannerService = (() => {
292
480
  delimiter: '/'
293
481
  };
294
482
  }
483
+ buildRouteDisplayItem(parsed) {
484
+ const { route, alreadyImported, parseError, format } = parsed;
485
+ const descr = route?.description ?? {};
486
+ const [C, U] = this.getUnitConversionShortcuts();
487
+ const distance = descr.distance === undefined ? undefined : {
488
+ value: C(descr.distance, 'distance', { digits: 1 }),
489
+ unit: U('distance')
490
+ };
491
+ return {
492
+ id: route.description.id,
493
+ distance,
494
+ label: route.title,
495
+ alreadyImported,
496
+ importable: parseError == null,
497
+ format,
498
+ errorReason: parseError
499
+ };
500
+ }
295
501
  getCompanionExts(parsers, primaryExt) {
296
502
  try {
297
503
  const matching = parsers.suppertsExtension(primaryExt);
@@ -306,12 +512,8 @@ let RouteLibraryScannerService = (() => {
306
512
  const dot = filename.lastIndexOf('.');
307
513
  return dot >= 0 ? filename.slice(dot + 1).toLowerCase() : '';
308
514
  }
309
- containsAbsolutePath(content) {
310
- if (/[>"']\//m.test(content))
311
- return true;
312
- if (/[A-Za-z]:[/\\]/m.test(content))
313
- return true;
314
- return false;
515
+ isMobile() {
516
+ return this.getBindings()?.appInfo?.getChannel() === 'mobile';
315
517
  }
316
518
  getRouteList() {
317
519
  return (0, service_2.useRouteList)();
@@ -319,6 +521,18 @@ let RouteLibraryScannerService = (() => {
319
521
  getBindings() {
320
522
  return (0, api_1.getBindings)();
321
523
  }
524
+ getRoutesDBLoader() {
525
+ return (0, db_1.useRoutesDbLoader)();
526
+ }
527
+ getParsers() {
528
+ return (0, parsers_1.useParsers)();
529
+ }
530
+ getUnitConversionShortcuts() {
531
+ return this.getUnitConverter().getUnitConversionShortcuts();
532
+ }
533
+ getUnitConverter() {
534
+ return (0, i18n_1.useUnitConverter)();
535
+ }
322
536
  };
323
537
  return RouteLibraryScannerService = _classThis;
324
538
  })();
@@ -37,7 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  return (mod && mod.__esModule) ? mod : { "default": mod };
38
38
  };
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.RoutesDbLoader = void 0;
40
+ 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");
@@ -339,3 +339,5 @@ let RoutesDbLoader = (() => {
339
339
  return RoutesDbLoader = _classThis;
340
340
  })();
341
341
  exports.RoutesDbLoader = RoutesDbLoader;
342
+ const useRoutesDbLoader = () => new RoutesDbLoader();
343
+ exports.useRoutesDbLoader = useRoutesDbLoader;
@@ -126,6 +126,7 @@ let RouteListService = (() => {
126
126
  syncInfo;
127
127
  currentView;
128
128
  stats;
129
+ isListUpdatePaused = false;
129
130
  constructor() {
130
131
  super('RouteList');
131
132
  this.myRoutes = new myroutes_1.MyRoutes('myRoutes', 'My Routes');
@@ -709,7 +710,15 @@ let RouteListService = (() => {
709
710
  this.logError(err, 'import', info);
710
711
  }
711
712
  }
713
+ pauseListUpdates() {
714
+ this.isListUpdatePaused = true;
715
+ }
716
+ resumeListUpdates() {
717
+ this.isListUpdatePaused = false;
718
+ }
712
719
  emitLists(event, props) {
720
+ if (this.isListUpdatePaused)
721
+ return;
713
722
  try {
714
723
  const { log, source = 'user' } = props ?? {};
715
724
  if (this.currentView === 'grid' || this.currentView === 'list') {
@@ -791,6 +800,25 @@ let RouteListService = (() => {
791
800
  return [];
792
801
  }
793
802
  }
803
+ existsBySourceUri(uri) {
804
+ try {
805
+ return this.routes.some(r => r.description?.sourceTreeUri === uri ||
806
+ r.description?.videoUrl === uri);
807
+ }
808
+ catch (err) {
809
+ this.logError(err, 'existsBySourceUri', { uri });
810
+ return false;
811
+ }
812
+ }
813
+ getBySourceUri(uri) {
814
+ try {
815
+ return this.routes.find(r => r.description?.sourceTreeUri === uri ||
816
+ r.description?.videoUrl === uri);
817
+ }
818
+ catch (err) {
819
+ this.logError(err, 'getBySourceUri', { uri });
820
+ }
821
+ }
794
822
  addRoute(route, source = 'system') {
795
823
  this.routes.push(route);
796
824
  if (route.description?.isDeleted) {