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
@@ -29,6 +29,9 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
29
29
  this.options.CommandOptions.push(
30
30
  { Name: '-u, --ultravisor [url]', Description: 'Connect to Ultravisor mesh. URL defaults to http://localhost:54321 if omitted.', Default: '' });
31
31
 
32
+ this.options.CommandOptions.push(
33
+ { Name: '--stack', Description: 'Run the full stack: spawn Ultravisor as a child process and connect to it. Uses XDG-style data paths under ~/.local/share and ~/.cache.', Default: false });
34
+
32
35
  this.options.CommandOptions.push(
33
36
  { Name: '-l, --logfile [path]', Description: 'Write logs to a file (auto-generates timestamped name if path omitted).', Default: '' });
34
37
 
@@ -78,14 +81,26 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
78
81
  }
79
82
 
80
83
  let tmpSelf = this;
81
- let tmpSetupServer = require('../RetoldRemote-Server-Setup.js');
82
-
83
84
  let tmpHashedFilenames = !(this.CommandOptions.noHash);
85
+ let tmpStackMode = !!this.CommandOptions.stack;
86
+
87
+ // Resolve XDG-style stack paths once (used by --stack mode)
88
+ let libStackLauncher = require('../RetoldRemote-Stack-Launcher.js');
89
+ let tmpStackPaths = libStackLauncher.resolveStackPaths();
90
+
91
+ // Cache root: explicit > stack default > package default
92
+ let tmpCacheRoot = null;
93
+ if (this.CommandOptions.cachePath)
94
+ {
95
+ tmpCacheRoot = libPath.resolve(this.CommandOptions.cachePath);
96
+ }
97
+ else if (tmpStackMode)
98
+ {
99
+ tmpCacheRoot = tmpStackPaths.RetoldCache;
100
+ }
84
101
 
85
- let tmpCacheRoot = this.CommandOptions.cachePath
86
- ? libPath.resolve(this.CommandOptions.cachePath)
87
- : null;
88
102
  let tmpCacheServer = this.CommandOptions.cacheServer || null;
103
+
89
104
  // -u with no URL → true (Commander behavior for [optional]), default to localhost
90
105
  let tmpUltravisorOpt = this.CommandOptions.ultravisor;
91
106
  let tmpUltravisorURL = null;
@@ -98,65 +113,125 @@ class RetoldRemoteCommandServe extends libCommandLineCommand
98
113
  tmpUltravisorURL = tmpUltravisorOpt;
99
114
  }
100
115
 
101
- tmpSetupServer(
102
- {
103
- ContentPath: tmpContentPath,
104
- DistPath: tmpDistPath,
105
- Port: tmpPort,
106
- HashedFilenames: tmpHashedFilenames,
107
- CacheRoot: tmpCacheRoot,
108
- CacheServer: tmpCacheServer,
109
- UltravisorURL: tmpUltravisorURL
110
- },
111
- function (pError, pServerInfo)
112
- {
113
- if (pError)
114
- {
115
- tmpSelf.log.error(`Failed to start server: ${pError.message}`);
116
- return fCallback(pError);
117
- }
118
-
119
- tmpSelf.log.info('');
120
- tmpSelf.log.info('==========================================================');
121
- tmpSelf.log.info(` Retold Remote running on http://localhost:${pServerInfo.Port}`);
122
- tmpSelf.log.info('==========================================================');
123
- tmpSelf.log.info(` Content: ${tmpContentPath}`);
124
- tmpSelf.log.info(` Assets: ${tmpDistPath}`);
125
- tmpSelf.log.info(` Browse: http://localhost:${pServerInfo.Port}/`);
126
- if (pServerInfo.UltravisorBeacon && pServerInfo.UltravisorBeacon.isEnabled())
127
- {
128
- tmpSelf.log.info(` Beacon: registered with Ultravisor at ${tmpUltravisorURL}`);
129
- }
130
- else if (tmpUltravisorURL)
116
+ // In stack mode, automatically set the ultravisor URL
117
+ if (tmpStackMode && !tmpUltravisorURL)
118
+ {
119
+ tmpUltravisorURL = 'http://localhost:54321';
120
+ }
121
+
122
+ // Hold the stack info so we can clean up on exit
123
+ let tmpStackInfo = null;
124
+
125
+ // Bind the actual server startup so we can call it after (optionally) launching ultravisor
126
+ let _startRetoldRemote = () =>
127
+ {
128
+ let tmpSetupServer = require('../RetoldRemote-Server-Setup.js');
129
+
130
+ tmpSetupServer(
131
131
  {
132
- tmpSelf.log.info(` Beacon: not connected (Ultravisor may be unreachable)`);
133
- }
134
- tmpSelf.log.info('==========================================================');
135
- tmpSelf.log.info('');
136
- tmpSelf.log.info(' Press Ctrl+C to stop.');
137
- tmpSelf.log.info('');
138
-
139
- // Graceful shutdown: disconnect beacon before exit
140
- process.on('SIGINT', () =>
132
+ ContentPath: tmpContentPath,
133
+ DistPath: tmpDistPath,
134
+ Port: tmpPort,
135
+ HashedFilenames: tmpHashedFilenames,
136
+ CacheRoot: tmpCacheRoot,
137
+ CacheServer: tmpCacheServer,
138
+ UltravisorURL: tmpUltravisorURL
139
+ },
140
+ function (pError, pServerInfo)
141
141
  {
142
+ if (pError)
143
+ {
144
+ tmpSelf.log.error(`Failed to start server: ${pError.message}`);
145
+ if (tmpStackInfo)
146
+ {
147
+ libStackLauncher.stop(tmpStackInfo, () => {});
148
+ }
149
+ return fCallback(pError);
150
+ }
151
+
142
152
  tmpSelf.log.info('');
143
- tmpSelf.log.info('Shutting down...');
153
+ tmpSelf.log.info('==========================================================');
154
+ tmpSelf.log.info(` Retold Remote running on http://localhost:${pServerInfo.Port}`);
155
+ tmpSelf.log.info('==========================================================');
156
+ tmpSelf.log.info(` Content: ${tmpContentPath}`);
157
+ tmpSelf.log.info(` Cache: ${tmpCacheRoot || '(default)'}`);
158
+ tmpSelf.log.info(` Browse: http://localhost:${pServerInfo.Port}/`);
144
159
  if (pServerInfo.UltravisorBeacon && pServerInfo.UltravisorBeacon.isEnabled())
145
160
  {
146
- pServerInfo.UltravisorBeacon.disconnectBeacon(() =>
147
- {
148
- process.exit(0);
149
- });
161
+ tmpSelf.log.info(` Beacon: registered with Ultravisor at ${tmpUltravisorURL}`);
162
+ }
163
+ else if (tmpUltravisorURL)
164
+ {
165
+ tmpSelf.log.info(` Beacon: not connected (Ultravisor may be unreachable)`);
150
166
  }
151
- else
167
+ if (tmpStackMode)
152
168
  {
153
- process.exit(0);
169
+ tmpSelf.log.info(` Stack: ultravisor + retold-remote (orator-conversion embedded)`);
154
170
  }
171
+ tmpSelf.log.info('==========================================================');
172
+ tmpSelf.log.info('');
173
+ tmpSelf.log.info(' Press Ctrl+C to stop.');
174
+ tmpSelf.log.info('');
175
+
176
+ // Graceful shutdown: disconnect beacon and stop child processes before exit
177
+ let _shutdown = () =>
178
+ {
179
+ tmpSelf.log.info('');
180
+ tmpSelf.log.info('Shutting down...');
181
+
182
+ let _finish = () =>
183
+ {
184
+ if (tmpStackInfo)
185
+ {
186
+ libStackLauncher.stop(tmpStackInfo, () => process.exit(0));
187
+ }
188
+ else
189
+ {
190
+ process.exit(0);
191
+ }
192
+ };
193
+
194
+ if (pServerInfo.UltravisorBeacon && pServerInfo.UltravisorBeacon.isEnabled())
195
+ {
196
+ pServerInfo.UltravisorBeacon.disconnectBeacon(_finish);
197
+ }
198
+ else
199
+ {
200
+ _finish();
201
+ }
202
+ };
203
+
204
+ process.on('SIGINT', _shutdown);
205
+ process.on('SIGTERM', _shutdown);
206
+
207
+ // Intentionally do NOT call fCallback() here.
208
+ // The server should keep running.
155
209
  });
210
+ };
156
211
 
157
- // Intentionally do NOT call fCallback() here.
158
- // The server should keep running.
159
- });
212
+ // In stack mode, launch ultravisor first, then start retold-remote
213
+ if (tmpStackMode)
214
+ {
215
+ libStackLauncher.start(
216
+ {
217
+ Logger: tmpSelf.log,
218
+ UltravisorPort: 54321
219
+ },
220
+ (pStackError, pStackInfo) =>
221
+ {
222
+ if (pStackError)
223
+ {
224
+ tmpSelf.log.error(`Stack launch failed: ${pStackError.message}`);
225
+ return fCallback(pStackError);
226
+ }
227
+ tmpStackInfo = pStackInfo;
228
+ _startRetoldRemote();
229
+ });
230
+ }
231
+ else
232
+ {
233
+ _startRetoldRemote();
234
+ }
160
235
  }
161
236
  }
162
237
 
@@ -47,6 +47,30 @@ module.exports =
47
47
  return this.addAudioSnippetToCollection(tmpTargetGUID);
48
48
  }
49
49
 
50
+ // If the image explorer is active with a selection, add as subimage
51
+ if (tmpRemote.ActiveMode === 'image-explorer')
52
+ {
53
+ let tmpIEX = this.pict.views['RetoldRemote-ImageExplorer'];
54
+ if (tmpIEX)
55
+ {
56
+ let tmpActiveSelection = tmpIEX.getActiveSelection();
57
+ if (tmpActiveSelection)
58
+ {
59
+ return this.addSubimageToCollection(tmpTargetGUID, tmpActiveSelection, tmpIEX._currentPath);
60
+ }
61
+ }
62
+ }
63
+
64
+ // If viewing a document (PDF/EPUB) with a pending region, add as document-region
65
+ if (tmpRemote.ActiveMode === 'viewer' && tmpRemote.CurrentViewerMediaType === 'document')
66
+ {
67
+ let tmpMediaViewer = this.pict.views['RetoldRemote-MediaViewer'];
68
+ if (tmpMediaViewer && tmpMediaViewer._pendingDocumentRegion)
69
+ {
70
+ return this.addDocumentRegionToCollection(tmpTargetGUID, tmpMediaViewer._pendingDocumentRegion, tmpRemote.CurrentViewerFile);
71
+ }
72
+ }
73
+
50
74
  let tmpCurrentItem = this._resolveCurrentItem();
51
75
 
52
76
  if (!tmpCurrentItem || !tmpCurrentItem.Path)
@@ -329,6 +353,148 @@ module.exports =
329
353
  return true;
330
354
  },
331
355
 
356
+ /**
357
+ * Add a subimage region (crop) to a collection.
358
+ *
359
+ * @param {string} pGUID - Collection GUID
360
+ * @param {object} pRegion - { X, Y, Width, Height, Label?, ID? } in image pixels
361
+ * @param {string} [pFilePath] - Explicit file path (defaults to current viewer file)
362
+ * @returns {boolean} true if the add was initiated
363
+ */
364
+ addSubimageToCollection: function addSubimageToCollection(pGUID, pRegion, pFilePath)
365
+ {
366
+ let tmpRemote = this._getRemote();
367
+ let tmpTargetGUID = pGUID || tmpRemote.LastUsedCollectionGUID;
368
+ if (!tmpTargetGUID || !pRegion)
369
+ {
370
+ return false;
371
+ }
372
+
373
+ let tmpFilePath = pFilePath || tmpRemote.CurrentViewerFile;
374
+ if (!tmpFilePath)
375
+ {
376
+ return false;
377
+ }
378
+
379
+ let tmpFileName = tmpFilePath.replace(/^.*\//, '');
380
+ let tmpLabel = pRegion.Label
381
+ ? pRegion.Label
382
+ : tmpFileName + ': ' + pRegion.Width + '\u00d7' + pRegion.Height + ' at ' + pRegion.X + ',' + pRegion.Y;
383
+
384
+ let tmpItem =
385
+ {
386
+ Type: 'image-crop',
387
+ Path: tmpFilePath,
388
+ CropRegion:
389
+ {
390
+ X: pRegion.X,
391
+ Y: pRegion.Y,
392
+ Width: pRegion.Width,
393
+ Height: pRegion.Height
394
+ },
395
+ Label: tmpLabel,
396
+ Note: ''
397
+ };
398
+
399
+ // If we have a hash for this file, include it
400
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
401
+ if (tmpProvider)
402
+ {
403
+ let tmpHash = tmpProvider.getHashForPath(tmpFilePath);
404
+ if (tmpHash)
405
+ {
406
+ tmpItem.Hash = tmpHash;
407
+ }
408
+ }
409
+
410
+ this.addItemsToCollection(tmpTargetGUID, [tmpItem]);
411
+ return true;
412
+ },
413
+
414
+ /**
415
+ * Add a document region (text selection or visual area) to a collection.
416
+ *
417
+ * @param {string} pGUID - Collection GUID
418
+ * @param {object} pRegion - Region object with Type, Label, and type-specific fields
419
+ * @param {string} [pFilePath] - Explicit file path (defaults to current viewer file)
420
+ * @returns {boolean} true if the add was initiated
421
+ */
422
+ addDocumentRegionToCollection: function addDocumentRegionToCollection(pGUID, pRegion, pFilePath)
423
+ {
424
+ let tmpRemote = this._getRemote();
425
+ let tmpTargetGUID = pGUID || tmpRemote.LastUsedCollectionGUID;
426
+ if (!tmpTargetGUID || !pRegion)
427
+ {
428
+ return false;
429
+ }
430
+
431
+ let tmpFilePath = pFilePath || tmpRemote.CurrentViewerFile;
432
+ if (!tmpFilePath)
433
+ {
434
+ return false;
435
+ }
436
+
437
+ let tmpFileName = tmpFilePath.replace(/^.*\//, '');
438
+ let tmpIsText = (pRegion.Type === 'text-selection');
439
+
440
+ // Build label
441
+ let tmpLabel = pRegion.Label || '';
442
+ if (!tmpLabel)
443
+ {
444
+ if (tmpIsText && pRegion.SelectedText)
445
+ {
446
+ tmpLabel = pRegion.SelectedText.substring(0, 50);
447
+ if (pRegion.SelectedText.length > 50) tmpLabel += '\u2026';
448
+ }
449
+ else
450
+ {
451
+ tmpLabel = tmpFileName;
452
+ }
453
+ if (pRegion.PageNumber)
454
+ {
455
+ tmpLabel = 'p.' + pRegion.PageNumber + ': ' + tmpLabel;
456
+ }
457
+ }
458
+
459
+ let tmpItem =
460
+ {
461
+ Type: 'document-region',
462
+ Path: tmpFilePath,
463
+ Label: tmpLabel,
464
+ Note: '',
465
+ DocumentRegionType: pRegion.Type || 'visual-region',
466
+ PageNumber: pRegion.PageNumber || null,
467
+ CFI: pRegion.CFI || null,
468
+ SelectedText: pRegion.SelectedText || null
469
+ };
470
+
471
+ // Include crop region for visual selections
472
+ if (!tmpIsText && pRegion.X !== undefined)
473
+ {
474
+ tmpItem.CropRegion =
475
+ {
476
+ X: pRegion.X,
477
+ Y: pRegion.Y,
478
+ Width: pRegion.Width,
479
+ Height: pRegion.Height
480
+ };
481
+ }
482
+
483
+ // File hash
484
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
485
+ if (tmpProvider)
486
+ {
487
+ let tmpHash = tmpProvider.getHashForPath(tmpFilePath);
488
+ if (tmpHash)
489
+ {
490
+ tmpItem.Hash = tmpHash;
491
+ }
492
+ }
493
+
494
+ this.addItemsToCollection(tmpTargetGUID, [tmpItem]);
495
+ return true;
496
+ },
497
+
332
498
  /**
333
499
  * Format a timestamp in seconds to a human-readable string.
334
500
  *
@@ -556,6 +556,15 @@ class GalleryNavigationProvider extends libPictProvider
556
556
  tmpRemote.CurrentViewerFile = '';
557
557
  tmpRemote.CurrentViewerMediaType = '';
558
558
 
559
+ // Notify the layout so any active sidebar tab (Info, Regions, etc.)
560
+ // transitions to its empty state instead of retaining stale content
561
+ // from the file the user just closed.
562
+ let tmpLayout = this.pict.views['ContentEditor-Layout'];
563
+ if (tmpLayout && typeof tmpLayout.notifyCurrentFileChanged === 'function')
564
+ {
565
+ tmpLayout.notifyCurrentFileChanged('');
566
+ }
567
+
559
568
  // Exit collection browsing mode
560
569
  tmpRemote.BrowsingCollection = false;
561
570
  tmpRemote.BrowsingCollectionIndex = -1;
@@ -630,6 +639,52 @@ class GalleryNavigationProvider extends libPictProvider
630
639
  tmpAEX.showExplorer(pItem.Path, pItem.AudioStart, pItem.AudioEnd);
631
640
  }
632
641
  }
642
+ else if (pItem.Type === 'document-region')
643
+ {
644
+ // Navigate to the document, then to the specific location
645
+ let tmpApp = this.pict.PictApplication;
646
+ if (tmpApp && tmpApp.navigateToFile)
647
+ {
648
+ tmpApp.navigateToFile(pItem.Path);
649
+
650
+ // After the viewer loads, navigate to the specific page/CFI
651
+ let tmpSelf = this;
652
+ setTimeout(() =>
653
+ {
654
+ let tmpMediaViewer = tmpSelf.pict.views['RetoldRemote-MediaViewer'];
655
+ if (tmpMediaViewer)
656
+ {
657
+ if (pItem.CFI && tmpMediaViewer._activeRendition)
658
+ {
659
+ tmpMediaViewer._activeRendition.display(pItem.CFI);
660
+ }
661
+ else if (pItem.PageNumber && typeof tmpMediaViewer._renderPdfPage === 'function')
662
+ {
663
+ tmpMediaViewer._renderPdfPage(pItem.PageNumber);
664
+ }
665
+ }
666
+ }, 1000);
667
+ }
668
+ }
669
+ else if (pItem.Type === 'image-crop' && pItem.CropRegion)
670
+ {
671
+ let tmpIEX = this.pict.views['RetoldRemote-ImageExplorer'];
672
+ if (tmpIEX)
673
+ {
674
+ tmpIEX.showExplorer(pItem.Path);
675
+ // Zoom to the crop region after the viewer loads
676
+ let tmpCrop = pItem.CropRegion;
677
+ setTimeout(() =>
678
+ {
679
+ if (tmpIEX._osdViewer && tmpIEX._dziData)
680
+ {
681
+ let tmpImageRect = new OpenSeadragon.Rect(tmpCrop.X, tmpCrop.Y, tmpCrop.Width, tmpCrop.Height);
682
+ let tmpViewportRect = tmpIEX._osdViewer.viewport.imageToViewportRectangle(tmpImageRect);
683
+ tmpIEX._osdViewer.viewport.fitBounds(tmpViewportRect);
684
+ }
685
+ }, 800);
686
+ }
687
+ }
633
688
  else if (pItem.Type === 'video-frame' && pItem.FrameCacheKey && pItem.FrameFilename)
634
689
  {
635
690
  // Show the cached frame image directly in the viewer