retold-remote 0.0.23 → 0.0.26

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 (79) hide show
  1. package/css/retold-remote.css +343 -20
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +64 -12
  4. package/docs/_cover.md +6 -6
  5. package/docs/_sidebar.md +2 -0
  6. package/docs/_topbar.md +1 -1
  7. package/docs/_version.json +7 -0
  8. package/docs/collections.md +30 -0
  9. package/docs/css/docuserve.css +327 -0
  10. package/docs/ebook-reader.md +75 -1
  11. package/docs/image-explorer.md +62 -2
  12. package/docs/index.html +39 -0
  13. package/docs/retold-catalog.json +254 -0
  14. package/docs/retold-keyword-index.json +31216 -0
  15. package/docs/server-setup.md +122 -91
  16. package/docs/stack-launcher.md +218 -0
  17. package/docs/synology.md +585 -0
  18. package/docs/ultravisor-configuration.md +5 -5
  19. package/docs/ultravisor-integration.md +4 -2
  20. package/package.json +20 -14
  21. package/source/Pict-Application-RetoldRemote.js +22 -0
  22. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  23. package/source/cli/RetoldRemote-Server-Setup.js +460 -7
  24. package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
  25. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  26. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  27. package/source/providers/CollectionManager-AddItems.js +166 -0
  28. package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
  29. package/source/providers/Pict-Provider-OperationStatus.js +597 -0
  30. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
  31. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  32. package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
  33. package/source/server/RetoldRemote-CollectionExportService.js +763 -0
  34. package/source/server/RetoldRemote-CollectionService.js +5 -0
  35. package/source/server/RetoldRemote-EbookService.js +218 -3
  36. package/source/server/RetoldRemote-ImageService.js +221 -46
  37. package/source/server/RetoldRemote-MediaService.js +63 -4
  38. package/source/server/RetoldRemote-MetadataCache.js +25 -5
  39. package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
  40. package/source/server/RetoldRemote-SubimageService.js +680 -0
  41. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  42. package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
  43. package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
  44. package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
  45. package/source/server/RetoldRemote-VideoFrameService.js +302 -9
  46. package/source/views/MediaViewer-EbookViewer.js +419 -1
  47. package/source/views/MediaViewer-PdfViewer.js +1050 -0
  48. package/source/views/PictView-Remote-AudioExplorer.js +77 -1
  49. package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
  50. package/source/views/PictView-Remote-Gallery.js +365 -64
  51. package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
  52. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  53. package/source/views/PictView-Remote-Layout.js +58 -0
  54. package/source/views/PictView-Remote-MediaViewer.js +100 -25
  55. package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
  56. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  57. package/source/views/PictView-Remote-TopBar.js +1 -0
  58. package/source/views/PictView-Remote-VideoExplorer.js +77 -1
  59. package/web-application/css/docuserve.css +277 -23
  60. package/web-application/css/retold-remote.css +343 -20
  61. package/web-application/docs/README.md +64 -12
  62. package/web-application/docs/_cover.md +6 -6
  63. package/web-application/docs/_sidebar.md +2 -0
  64. package/web-application/docs/_topbar.md +1 -1
  65. package/web-application/docs/collections.md +30 -0
  66. package/web-application/docs/ebook-reader.md +75 -1
  67. package/web-application/docs/image-explorer.md +62 -2
  68. package/web-application/docs/server-setup.md +122 -91
  69. package/web-application/docs/stack-launcher.md +218 -0
  70. package/web-application/docs/synology.md +585 -0
  71. package/web-application/docs/ultravisor-configuration.md +5 -5
  72. package/web-application/docs/ultravisor-integration.md +4 -2
  73. package/web-application/js/pict-docuserve.min.js +12 -12
  74. package/web-application/js/pict.min.js +2 -2
  75. package/web-application/js/pict.min.js.map +1 -1
  76. package/web-application/retold-remote.js +6596 -1784
  77. package/web-application/retold-remote.js.map +1 -1
  78. package/web-application/retold-remote.min.js +75 -23
  79. package/web-application/retold-remote.min.js.map +1 -1
@@ -0,0 +1,353 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "RetoldRemote-SubimagesPanel",
6
+ DefaultRenderable: "RetoldRemote-SubimagesPanel",
7
+ DefaultDestinationAddress: "#RetoldRemote-Subimages-Container",
8
+ AutoRender: false,
9
+
10
+ CSS: ``
11
+ };
12
+
13
+ /**
14
+ * Subimages Panel — sidebar tab showing labeled subimage regions
15
+ * for the currently viewed image file.
16
+ *
17
+ * Regions are fetched from the SubimageService API and displayed
18
+ * as a list with label, dimensions, and action buttons.
19
+ */
20
+ class RetoldRemoteSubimagesPanelView extends libPictView
21
+ {
22
+ constructor(pFable, pOptions, pServiceHash)
23
+ {
24
+ super(pFable, pOptions, pServiceHash);
25
+ this._regions = [];
26
+ this._currentPath = '';
27
+ }
28
+
29
+ /**
30
+ * Render the subimages panel for the current file.
31
+ */
32
+ render()
33
+ {
34
+ let tmpContainer = document.getElementById('RetoldRemote-Subimages-Container');
35
+ if (!tmpContainer)
36
+ {
37
+ return;
38
+ }
39
+
40
+ let tmpRemote = this.pict.AppData.RetoldRemote;
41
+ let tmpFilePath = tmpRemote.CurrentViewerFile;
42
+ let tmpMediaType = tmpRemote.CurrentViewerMediaType;
43
+
44
+ // Show for images and documents (EPUB, PDF, CBZ pages)
45
+ if (!tmpFilePath)
46
+ {
47
+ tmpContainer.innerHTML = '<div style="padding:12px;color:var(--retold-text-dim);font-size:0.82rem;">View a file to see its regions.</div>';
48
+ this._regions = [];
49
+ this._currentPath = '';
50
+ return;
51
+ }
52
+
53
+ // If the file changed, fetch regions
54
+ if (tmpFilePath !== this._currentPath)
55
+ {
56
+ this._currentPath = tmpFilePath;
57
+ this._fetchAndRender(tmpFilePath, tmpContainer);
58
+ return;
59
+ }
60
+
61
+ // Re-render with cached regions (e.g. after add/delete)
62
+ this._renderRegionList(tmpContainer);
63
+ }
64
+
65
+ /**
66
+ * Force a refresh from the server for the current file.
67
+ */
68
+ refresh()
69
+ {
70
+ this._currentPath = '';
71
+ this.render();
72
+ }
73
+
74
+ /**
75
+ * Fetch regions from the server and render.
76
+ *
77
+ * @param {string} pFilePath - Relative file path
78
+ * @param {HTMLElement} pContainer - The container to render into
79
+ */
80
+ _fetchAndRender(pFilePath, pContainer)
81
+ {
82
+ let tmpSelf = this;
83
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
84
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
85
+
86
+ pContainer.innerHTML = '<div style="padding:12px;color:var(--retold-text-dim);font-size:0.82rem;">Loading\u2026</div>';
87
+
88
+ fetch('/api/media/subimage-regions?path=' + tmpPathParam)
89
+ .then((pResponse) => pResponse.json())
90
+ .then((pResult) =>
91
+ {
92
+ if (pResult && pResult.Success)
93
+ {
94
+ tmpSelf._regions = pResult.Regions || [];
95
+ }
96
+ else
97
+ {
98
+ tmpSelf._regions = [];
99
+ }
100
+ tmpSelf._renderRegionList(pContainer);
101
+ })
102
+ .catch(() =>
103
+ {
104
+ tmpSelf._regions = [];
105
+ tmpSelf._renderRegionList(pContainer);
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Render the region list into the container.
111
+ *
112
+ * @param {HTMLElement} pContainer - The container element
113
+ */
114
+ _renderRegionList(pContainer)
115
+ {
116
+ if (!pContainer)
117
+ {
118
+ pContainer = document.getElementById('RetoldRemote-Subimages-Container');
119
+ }
120
+ if (!pContainer)
121
+ {
122
+ return;
123
+ }
124
+
125
+ // Also sync the regions to the image explorer if it's open
126
+ let tmpIEX = this.pict.views['RetoldRemote-ImageExplorer'];
127
+ if (tmpIEX && tmpIEX._currentPath === this._currentPath)
128
+ {
129
+ tmpIEX._savedRegions = this._regions;
130
+ }
131
+
132
+ let tmpFmt = this.pict.providers['RetoldRemote-FormattingUtilities'];
133
+ let tmpFileName = (this._currentPath || '').replace(/^.*\//, '');
134
+
135
+ let tmpHTML = '<div class="retold-remote-subimages-panel">';
136
+
137
+ // Header
138
+ tmpHTML += '<div style="padding:8px 10px;font-size:0.78rem;color:var(--retold-text-dim);border-bottom:1px solid var(--retold-border);">';
139
+ tmpHTML += tmpFmt.escapeHTML(tmpFileName);
140
+ tmpHTML += ' &mdash; ' + this._regions.length + ' region' + (this._regions.length !== 1 ? 's' : '');
141
+ tmpHTML += '</div>';
142
+
143
+ if (this._regions.length === 0)
144
+ {
145
+ tmpHTML += '<div style="padding:16px 12px;color:var(--retold-text-dim);font-size:0.8rem;text-align:center;">';
146
+ tmpHTML += 'No regions yet.<br>Use selection tools in the viewer to create regions.';
147
+ tmpHTML += '</div>';
148
+ }
149
+ else
150
+ {
151
+ for (let i = 0; i < this._regions.length; i++)
152
+ {
153
+ let tmpRegion = this._regions[i];
154
+ let tmpLabel = tmpRegion.Label || '(unlabeled)';
155
+ let tmpIsText = (tmpRegion.Type === 'text-selection');
156
+
157
+ // Build description based on region type
158
+ let tmpDesc = '';
159
+ if (tmpIsText)
160
+ {
161
+ let tmpPreview = (tmpRegion.SelectedText || '').substring(0, 60);
162
+ if ((tmpRegion.SelectedText || '').length > 60) tmpPreview += '\u2026';
163
+ tmpDesc = tmpPreview || '(no text)';
164
+ if (tmpRegion.PageNumber)
165
+ {
166
+ tmpDesc = 'p.' + tmpRegion.PageNumber + ' \u2014 ' + tmpDesc;
167
+ }
168
+ else if (tmpRegion.ChapterTitle)
169
+ {
170
+ tmpDesc = tmpRegion.ChapterTitle;
171
+ }
172
+ }
173
+ else
174
+ {
175
+ tmpDesc = (tmpRegion.Width || 0) + ' \u00d7 ' + (tmpRegion.Height || 0) + ' px';
176
+ if (tmpRegion.PageNumber)
177
+ {
178
+ tmpDesc = 'p.' + tmpRegion.PageNumber + ' \u2014 ' + tmpDesc;
179
+ }
180
+ else if (tmpRegion.X !== null && tmpRegion.Y !== null)
181
+ {
182
+ tmpDesc += ' at ' + tmpRegion.X + ', ' + tmpRegion.Y;
183
+ }
184
+ }
185
+
186
+ // Type icon
187
+ let tmpTypeIcon = tmpIsText ? '\uD83D\uDCDD' : '\uD83D\uDD32';
188
+
189
+ tmpHTML += '<div class="retold-remote-subimages-item" data-region-id="' + tmpRegion.ID + '">';
190
+
191
+ // Region info
192
+ tmpHTML += '<div class="retold-remote-subimages-item-info">';
193
+ tmpHTML += '<div class="retold-remote-subimages-item-label">' + tmpTypeIcon + ' ' + tmpFmt.escapeHTML(tmpLabel) + '</div>';
194
+ tmpHTML += '<div class="retold-remote-subimages-item-dims">' + tmpFmt.escapeHTML(tmpDesc) + '</div>';
195
+ tmpHTML += '</div>';
196
+
197
+ // Actions
198
+ tmpHTML += '<div class="retold-remote-subimages-item-actions">';
199
+ tmpHTML += '<button onclick="pict.views[\'RetoldRemote-SubimagesPanel\'].navigateToRegion(\'' + tmpRegion.ID + '\')" title="Navigate to region" style="font-size:0.7rem;padding:2px 6px;">&#128270;</button>';
200
+ tmpHTML += '<button onclick="pict.views[\'RetoldRemote-SubimagesPanel\'].addRegionToCollection(\'' + tmpRegion.ID + '\')" title="Add to collection" style="font-size:0.7rem;padding:2px 6px;">&#10010;</button>';
201
+ tmpHTML += '<button onclick="pict.views[\'RetoldRemote-SubimagesPanel\'].deleteRegion(\'' + tmpRegion.ID + '\')" title="Delete region" style="font-size:0.7rem;padding:2px 6px;">&#128465;</button>';
202
+ tmpHTML += '</div>';
203
+
204
+ tmpHTML += '</div>';
205
+ }
206
+ }
207
+
208
+ tmpHTML += '</div>';
209
+ pContainer.innerHTML = tmpHTML;
210
+ }
211
+
212
+ /**
213
+ * Navigate to a specific region — handles images, EPUB, and PDF.
214
+ *
215
+ * @param {string} pRegionID - The region ID
216
+ */
217
+ navigateToRegion(pRegionID)
218
+ {
219
+ let tmpRegion = null;
220
+ for (let i = 0; i < this._regions.length; i++)
221
+ {
222
+ if (this._regions[i].ID === pRegionID)
223
+ {
224
+ tmpRegion = this._regions[i];
225
+ break;
226
+ }
227
+ }
228
+ if (!tmpRegion)
229
+ {
230
+ return;
231
+ }
232
+
233
+ let tmpRemote = this.pict.AppData.RetoldRemote;
234
+ let tmpMediaViewer = this.pict.views['RetoldRemote-MediaViewer'];
235
+
236
+ // EPUB: navigate to CFI location
237
+ if (tmpRegion.CFI && tmpMediaViewer && tmpMediaViewer._activeRendition)
238
+ {
239
+ tmpMediaViewer._activeRendition.display(tmpRegion.CFI);
240
+ return;
241
+ }
242
+
243
+ // PDF: navigate to page
244
+ if (tmpRegion.PageNumber && tmpMediaViewer && typeof tmpMediaViewer._renderPdfPage === 'function')
245
+ {
246
+ tmpMediaViewer._renderPdfPage(tmpRegion.PageNumber);
247
+ return;
248
+ }
249
+
250
+ // Image: use image explorer
251
+ let tmpIEX = this.pict.views['RetoldRemote-ImageExplorer'];
252
+ if (tmpRemote.ActiveMode === 'image-explorer' && tmpIEX)
253
+ {
254
+ tmpIEX.zoomToRegion(pRegionID);
255
+ }
256
+ else if (tmpIEX && this._currentPath)
257
+ {
258
+ tmpIEX.showExplorer(this._currentPath);
259
+ setTimeout(() =>
260
+ {
261
+ tmpIEX.zoomToRegion(pRegionID);
262
+ }, 800);
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Add a subimage region to the active collection.
268
+ *
269
+ * @param {string} pRegionID - The region ID to add
270
+ */
271
+ addRegionToCollection(pRegionID)
272
+ {
273
+ let tmpRegion = null;
274
+ for (let i = 0; i < this._regions.length; i++)
275
+ {
276
+ if (this._regions[i].ID === pRegionID)
277
+ {
278
+ tmpRegion = this._regions[i];
279
+ break;
280
+ }
281
+ }
282
+
283
+ if (!tmpRegion)
284
+ {
285
+ return;
286
+ }
287
+
288
+ let tmpCollMgr = this.pict.providers['RetoldRemote-CollectionManager'];
289
+ if (tmpCollMgr)
290
+ {
291
+ let tmpGUID = tmpCollMgr.getQuickAddTargetGUID();
292
+ if (tmpGUID)
293
+ {
294
+ tmpCollMgr.addSubimageToCollection(tmpGUID, tmpRegion, this._currentPath);
295
+ }
296
+ else
297
+ {
298
+ let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
299
+ if (tmpTopBar && typeof tmpTopBar.showAddToCollectionDropdown === 'function')
300
+ {
301
+ tmpTopBar.showAddToCollectionDropdown();
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Delete a subimage region.
309
+ *
310
+ * @param {string} pRegionID - The region ID to delete
311
+ */
312
+ deleteRegion(pRegionID)
313
+ {
314
+ let tmpIEX = this.pict.views['RetoldRemote-ImageExplorer'];
315
+ if (tmpIEX && tmpIEX._currentPath === this._currentPath)
316
+ {
317
+ // Delegate to the explorer which handles the API call and overlay removal
318
+ tmpIEX.deleteRegion(pRegionID);
319
+ }
320
+ else
321
+ {
322
+ // Delete directly via API
323
+ let tmpSelf = this;
324
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
325
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._currentPath) : encodeURIComponent(this._currentPath);
326
+
327
+ fetch('/api/media/subimage-regions/' + encodeURIComponent(pRegionID) + '?path=' + tmpPathParam,
328
+ {
329
+ method: 'DELETE'
330
+ })
331
+ .then((pResponse) => pResponse.json())
332
+ .then((pResult) =>
333
+ {
334
+ if (pResult && pResult.Success)
335
+ {
336
+ tmpSelf._regions = pResult.Regions || [];
337
+ tmpSelf._renderRegionList();
338
+
339
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
340
+ if (tmpToast)
341
+ {
342
+ tmpToast.showToast('Region deleted');
343
+ }
344
+ }
345
+ })
346
+ .catch(() => {});
347
+ }
348
+ }
349
+ }
350
+
351
+ RetoldRemoteSubimagesPanelView.default_configuration = _ViewConfiguration;
352
+
353
+ module.exports = RetoldRemoteSubimagesPanelView;
@@ -23,6 +23,7 @@ const _ViewConfiguration =
23
23
  <div class="retold-remote-topbar-info" id="RetoldRemote-TopBar-Info"></div>
24
24
  <div class="retold-remote-topbar-actions">
25
25
  <button class="retold-remote-topbar-aisort-btn" id="RetoldRemote-TopBar-AISortBtn" onclick="pict.views['ContentEditor-TopBar'].triggerAISort()" title="AI Sort (generate sort plan for current folder)" style="display:none;">Ai</button>
26
+ <button class="retold-remote-topbar-btn retold-remote-topbar-regions-btn" id="RetoldRemote-TopBar-RegionsBtn" onclick="pict.views['RetoldRemote-RegionsBrowser'] && pict.views['RetoldRemote-RegionsBrowser'].open()" title="Browse all regions by folder">&#9635;</button>
26
27
  <button class="retold-remote-topbar-btn retold-remote-topbar-addcoll-btn" id="RetoldRemote-TopBar-AddToCollectionBtn" onclick="pict.views['ContentEditor-TopBar'].addToCollection(event)" title="Add to collection">&#9733;</button>
27
28
  <button class="retold-remote-topbar-btn retold-remote-topbar-favorites-btn" id="RetoldRemote-TopBar-FavoritesBtn" onclick="pict.views['ContentEditor-TopBar'].toggleFavorite()" title="Toggle favorite (h)">&#9825;</button>
28
29
  <button class="retold-remote-topbar-sidebar-toggle retold-remote-topbar-collections-btn" id="RetoldRemote-TopBar-CollectionsBtn" onclick="pict.views['ContentEditor-TopBar'].toggleCollections()" title="Toggle Collections panel (b)">&#9733;</button>
@@ -203,6 +203,14 @@ class RetoldRemoteVideoExplorerView extends libPictView
203
203
  tmpRemote.CurrentViewerFile = pFilePath;
204
204
  tmpRemote.CurrentViewerMediaType = 'video';
205
205
  this._currentPath = pFilePath;
206
+
207
+ // Notify the layout so active sidebar panels (Info, Regions, etc.)
208
+ // refresh to the new file instead of keeping stale content.
209
+ let tmpLayout = this.pict.views['ContentEditor-Layout'];
210
+ if (tmpLayout && typeof tmpLayout.notifyCurrentFileChanged === 'function')
211
+ {
212
+ tmpLayout.notifyCurrentFileChanged(pFilePath);
213
+ }
206
214
  this._frameData = null;
207
215
  this._selectedFrameIndex = -1;
208
216
  this._customFrames = [];
@@ -357,16 +365,54 @@ class RetoldRemoteVideoExplorerView extends libPictView
357
365
  tmpURL += '&width=1920&height=1080';
358
366
  }
359
367
 
360
- fetch(tmpURL)
368
+ // Cancel any previous in-flight frame fetch (e.g. switching videos fast)
369
+ this._cancelActiveOperation();
370
+
371
+ // Start a new operation entry for this extraction
372
+ let tmpStatus = this.pict.providers['RetoldRemote-OperationStatus'];
373
+ let tmpOp = tmpStatus ? tmpStatus.startOperation(
374
+ {
375
+ Label: 'Extracting video frames',
376
+ Phase: 'Starting…',
377
+ Cancelable: true
378
+ }) : null;
379
+ if (tmpOp)
380
+ {
381
+ this._activeOperationId = tmpOp.OperationId;
382
+ this._activeAbortController = tmpOp.AbortController;
383
+ }
384
+
385
+ let tmpFetchOptions = {};
386
+ if (tmpOp && tmpOp.AbortController)
387
+ {
388
+ tmpFetchOptions.signal = tmpOp.AbortController.signal;
389
+ }
390
+ if (tmpOp)
391
+ {
392
+ tmpFetchOptions.headers = { 'X-Op-Id': tmpOp.OperationId };
393
+ }
394
+
395
+ fetch(tmpURL, tmpFetchOptions)
361
396
  .then((pResponse) => pResponse.json())
362
397
  .then((pData) =>
363
398
  {
364
399
  if (!pData || !pData.Success)
365
400
  {
401
+ if (tmpOp && tmpStatus)
402
+ {
403
+ tmpStatus.errorOperation(tmpOp.OperationId, { message: pData ? pData.Error : 'Unknown error' });
404
+ }
366
405
  tmpSelf._showError(pData ? pData.Error : 'Unknown error');
367
406
  return;
368
407
  }
369
408
 
409
+ if (tmpOp && tmpStatus)
410
+ {
411
+ tmpStatus.completeOperation(tmpOp.OperationId);
412
+ }
413
+ tmpSelf._activeOperationId = null;
414
+ tmpSelf._activeAbortController = null;
415
+
370
416
  tmpSelf._frameData = pData;
371
417
  tmpSelf._renderFrames();
372
418
 
@@ -375,10 +421,38 @@ class RetoldRemoteVideoExplorerView extends libPictView
375
421
  })
376
422
  .catch((pError) =>
377
423
  {
424
+ // Silent abort — user navigated away or switched videos
425
+ if (pError && pError.name === 'AbortError')
426
+ {
427
+ return;
428
+ }
429
+ if (tmpOp && tmpStatus)
430
+ {
431
+ tmpStatus.errorOperation(tmpOp.OperationId, pError);
432
+ }
378
433
  tmpSelf._showError(pError.message);
379
434
  });
380
435
  }
381
436
 
437
+ /**
438
+ * Cancel any in-flight frame extraction for this explorer. Used on
439
+ * navigate-away and when starting a fresh extraction.
440
+ */
441
+ _cancelActiveOperation()
442
+ {
443
+ if (this._activeAbortController)
444
+ {
445
+ try { this._activeAbortController.abort(); } catch (pErr) { /* ignore */ }
446
+ }
447
+ let tmpStatus = this.pict.providers['RetoldRemote-OperationStatus'];
448
+ if (this._activeOperationId && tmpStatus)
449
+ {
450
+ tmpStatus.cancelOperation(this._activeOperationId);
451
+ }
452
+ this._activeOperationId = null;
453
+ this._activeAbortController = null;
454
+ }
455
+
382
456
  /**
383
457
  * Render the extracted frames into the grid.
384
458
  */
@@ -698,6 +772,7 @@ class RetoldRemoteVideoExplorerView extends libPictView
698
772
  */
699
773
  goBack()
700
774
  {
775
+ this._cancelActiveOperation();
701
776
  this._cleanupWindowListeners();
702
777
 
703
778
  let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
@@ -712,6 +787,7 @@ class RetoldRemoteVideoExplorerView extends libPictView
712
787
  */
713
788
  playInBrowser()
714
789
  {
790
+ this._cancelActiveOperation();
715
791
  this._cleanupWindowListeners();
716
792
 
717
793
  let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];