retold-remote 0.0.13 → 0.0.17

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 (30) hide show
  1. package/css/retold-remote.css +75 -0
  2. package/docs/_sidebar.md +2 -0
  3. package/docs/ultravisor-configuration.md +212 -0
  4. package/docs/ultravisor-integration.md +140 -0
  5. package/package.json +121 -96
  6. package/source/cli/RetoldRemote-Server-Setup.js +26 -0
  7. package/source/providers/Pict-Provider-GalleryNavigation.js +11 -3
  8. package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +5 -0
  9. package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +16 -0
  10. package/source/server/RetoldRemote-AudioWaveformService.js +101 -23
  11. package/source/server/RetoldRemote-EbookService.js +119 -6
  12. package/source/server/RetoldRemote-ImageService.js +254 -2
  13. package/source/server/RetoldRemote-MediaService.js +274 -37
  14. package/source/server/RetoldRemote-ToolDetector.js +27 -3
  15. package/source/server/RetoldRemote-UltravisorDispatcher.js +599 -0
  16. package/source/server/RetoldRemote-VideoFrameService.js +309 -77
  17. package/source/views/PictView-Remote-AudioExplorer.js +28 -14
  18. package/source/views/PictView-Remote-ImageExplorer.js +31 -11
  19. package/source/views/PictView-Remote-VLCSetup.js +22 -12
  20. package/source/views/PictView-Remote-VideoExplorer.js +29 -14
  21. package/web-application/css/retold-remote.css +75 -0
  22. package/web-application/docs/_sidebar.md +2 -0
  23. package/web-application/docs/ultravisor-configuration.md +212 -0
  24. package/web-application/docs/ultravisor-integration.md +140 -0
  25. package/web-application/js/pict.min.js +2 -2
  26. package/web-application/js/pict.min.js.map +1 -1
  27. package/web-application/retold-remote.js +4763 -4238
  28. package/web-application/retold-remote.js.map +1 -1
  29. package/web-application/retold-remote.min.js +16 -16
  30. package/web-application/retold-remote.min.js.map +1 -1
@@ -51,6 +51,9 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
51
51
 
52
52
  this.contentPath = libPath.resolve(this.options.ContentPath);
53
53
 
54
+ // Ultravisor dispatcher — set via setDispatcher()
55
+ this._dispatcher = null;
56
+
54
57
  // Apply explorer state persistence mixin (initializeState,
55
58
  // _buildExplorerStateKey, loadExplorerState, saveExplorerState)
56
59
  libExplorerStateMixin.apply(this, EXPLORER_STATE_SOURCE, 'video-explorer');
@@ -62,6 +65,16 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
62
65
  this.fable.log.info('Video Frame Service: frames in ParimeBinaryStorage, state in Bibliograph');
63
66
  }
64
67
 
68
+ /**
69
+ * Set the Ultravisor dispatcher for offloading heavy processing.
70
+ *
71
+ * @param {object} pDispatcher - RetoldRemoteUltravisorDispatcher instance
72
+ */
73
+ setDispatcher(pDispatcher)
74
+ {
75
+ this._dispatcher = pDispatcher;
76
+ }
77
+
65
78
  /**
66
79
  * Install video-specific explorer state methods that need read-merge-write
67
80
  * behavior (merging custom frames, preserving selection across saves).
@@ -218,43 +231,76 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
218
231
 
219
232
  /**
220
233
  * Probe a video file with ffprobe to get its duration.
234
+ * Tries Ultravisor dispatch first, falls back to local execution.
221
235
  *
222
236
  * @param {string} pAbsPath - Absolute path to the video
223
237
  * @param {Function} fCallback - Callback(pError, { duration, width, height, codec })
224
238
  */
225
239
  _probeVideo(pAbsPath, fCallback)
226
240
  {
227
- try
228
- {
229
- let tmpCmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${pAbsPath}"`;
230
- let tmpOutput = libChildProcess.execSync(tmpCmd, { maxBuffer: 1024 * 1024, timeout: 15000 });
231
- let tmpData = JSON.parse(tmpOutput.toString());
232
-
233
- let tmpResult = {};
241
+ let tmpSelf = this;
234
242
 
235
- if (tmpData.format)
243
+ // Try Ultravisor dispatch first
244
+ if (this._dispatcher && this._dispatcher.isAvailable())
245
+ {
246
+ let tmpRelPath;
247
+ try
248
+ {
249
+ tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
250
+ }
251
+ catch (pErr)
236
252
  {
237
- tmpResult.duration = parseFloat(tmpData.format.duration) || null;
238
- tmpResult.bitrate = parseInt(tmpData.format.bit_rate, 10) || null;
239
- tmpResult.size = parseInt(tmpData.format.size, 10) || null;
253
+ tmpRelPath = null;
240
254
  }
241
255
 
242
- if (tmpData.streams)
256
+ if (tmpRelPath && !tmpRelPath.startsWith('..'))
243
257
  {
244
- for (let i = 0; i < tmpData.streams.length; i++)
258
+ let tmpCommand = `ffprobe -v quiet -print_format json -show_format -show_streams "{SourcePath}"`;
259
+
260
+ this._dispatcher.dispatchMediaCommand(
261
+ {
262
+ Command: tmpCommand,
263
+ InputPath: tmpRelPath,
264
+ AffinityKey: tmpRelPath,
265
+ TimeoutMs: 30000
266
+ },
267
+ (pDispatchError, pResult) =>
245
268
  {
246
- let tmpStream = tmpData.streams[i];
247
- if (tmpStream.codec_type === 'video')
269
+ if (!pDispatchError && pResult && pResult.Outputs && pResult.Outputs.StdOut)
248
270
  {
249
- tmpResult.width = tmpStream.width;
250
- tmpResult.height = tmpStream.height;
251
- tmpResult.codec = tmpStream.codec_name;
252
- break;
271
+ try
272
+ {
273
+ let tmpData = JSON.parse(pResult.Outputs.StdOut);
274
+ let tmpParsed = tmpSelf._parseProbeData(tmpData);
275
+ tmpSelf.fable.log.info(`ffprobe via Ultravisor for ${tmpRelPath}`);
276
+ return fCallback(null, tmpParsed);
277
+ }
278
+ catch (pParseError)
279
+ {
280
+ // Fall through to local
281
+ }
253
282
  }
254
- }
283
+
284
+ tmpSelf._probeVideoLocal(pAbsPath, fCallback);
285
+ });
286
+ return;
255
287
  }
288
+ }
289
+
290
+ return this._probeVideoLocal(pAbsPath, fCallback);
291
+ }
256
292
 
257
- return fCallback(null, tmpResult);
293
+ /**
294
+ * Probe a video file locally with ffprobe.
295
+ */
296
+ _probeVideoLocal(pAbsPath, fCallback)
297
+ {
298
+ try
299
+ {
300
+ let tmpCmd = `ffprobe -v quiet -print_format json -show_format -show_streams "${pAbsPath}"`;
301
+ let tmpOutput = libChildProcess.execSync(tmpCmd, { maxBuffer: 1024 * 1024, timeout: 15000 });
302
+ let tmpData = JSON.parse(tmpOutput.toString());
303
+ return fCallback(null, this._parseProbeData(tmpData));
258
304
  }
259
305
  catch (pError)
260
306
  {
@@ -262,8 +308,41 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
262
308
  }
263
309
  }
264
310
 
311
+ /**
312
+ * Parse ffprobe JSON output into a normalized result object.
313
+ */
314
+ _parseProbeData(pData)
315
+ {
316
+ let tmpResult = {};
317
+
318
+ if (pData.format)
319
+ {
320
+ tmpResult.duration = parseFloat(pData.format.duration) || null;
321
+ tmpResult.bitrate = parseInt(pData.format.bit_rate, 10) || null;
322
+ tmpResult.size = parseInt(pData.format.size, 10) || null;
323
+ }
324
+
325
+ if (pData.streams)
326
+ {
327
+ for (let i = 0; i < pData.streams.length; i++)
328
+ {
329
+ let tmpStream = pData.streams[i];
330
+ if (tmpStream.codec_type === 'video')
331
+ {
332
+ tmpResult.width = tmpStream.width;
333
+ tmpResult.height = tmpStream.height;
334
+ tmpResult.codec = tmpStream.codec_name;
335
+ break;
336
+ }
337
+ }
338
+ }
339
+
340
+ return tmpResult;
341
+ }
342
+
265
343
  /**
266
344
  * Extract a single frame from a video at a given timestamp.
345
+ * Synchronous version for backward compatibility.
267
346
  *
268
347
  * @param {string} pAbsPath - Absolute path to the video
269
348
  * @param {number} pTimestamp - Timestamp in seconds
@@ -274,17 +353,18 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
274
353
  * @returns {boolean} True if extraction succeeded
275
354
  */
276
355
  _extractFrame(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat)
356
+ {
357
+ return this._extractFrameLocal(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat);
358
+ }
359
+
360
+ /**
361
+ * Extract a single frame locally using ffmpeg (synchronous).
362
+ */
363
+ _extractFrameLocal(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat)
277
364
  {
278
365
  try
279
366
  {
280
- // Format timestamp as HH:MM:SS.mmm for ffmpeg
281
- let tmpHours = Math.floor(pTimestamp / 3600);
282
- let tmpMinutes = Math.floor((pTimestamp % 3600) / 60);
283
- let tmpSeconds = pTimestamp % 60;
284
- let tmpTimeStr = `${String(tmpHours).padStart(2, '0')}:${String(tmpMinutes).padStart(2, '0')}:${tmpSeconds.toFixed(3).padStart(6, '0')}`;
285
-
286
- let tmpCodec = (pFormat === 'png') ? 'png' : (pFormat === 'webp') ? 'webp' : 'mjpeg';
287
-
367
+ let tmpTimeStr = this._formatFfmpegTimestamp(pTimestamp);
288
368
  let tmpMuxer = (pFormat === 'png') ? 'image2' : (pFormat === 'webp') ? 'webp' : 'mjpeg';
289
369
  let tmpCmd = `ffmpeg -ss ${tmpTimeStr} -i "${pAbsPath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -f ${tmpMuxer} -y "${pOutputPath}"`;
290
370
  libChildProcess.execSync(tmpCmd, { stdio: 'ignore', timeout: 30000 });
@@ -297,6 +377,97 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
297
377
  }
298
378
  }
299
379
 
380
+ /**
381
+ * Extract a single frame asynchronously, trying Ultravisor dispatch first.
382
+ *
383
+ * @param {string} pAbsPath - Absolute path to the video
384
+ * @param {number} pTimestamp - Timestamp in seconds
385
+ * @param {string} pOutputPath - Absolute path for the output image
386
+ * @param {number} pWidth - Target width
387
+ * @param {number} pHeight - Target height
388
+ * @param {string} pFormat - Output format (jpg, png, webp)
389
+ * @param {Function} fCallback - Callback(pError, pSuccess)
390
+ */
391
+ _extractFrameAsync(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat, fCallback)
392
+ {
393
+ let tmpSelf = this;
394
+
395
+ // Try Ultravisor dispatch first
396
+ if (this._dispatcher && this._dispatcher.isAvailable())
397
+ {
398
+ let tmpRelPath;
399
+ try
400
+ {
401
+ tmpRelPath = libPath.relative(this.contentPath, pAbsPath);
402
+ }
403
+ catch (pErr)
404
+ {
405
+ tmpRelPath = null;
406
+ }
407
+
408
+ if (tmpRelPath && !tmpRelPath.startsWith('..'))
409
+ {
410
+ let tmpTimeStr = this._formatFfmpegTimestamp(pTimestamp);
411
+ let tmpMuxer = (pFormat === 'png') ? 'image2' : (pFormat === 'webp') ? 'webp' : 'mjpeg';
412
+ let tmpOutputFilename = libPath.basename(pOutputPath);
413
+ let tmpCommand = `ffmpeg -ss ${tmpTimeStr} -i "{SourcePath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -f ${tmpMuxer} -y "{OutputPath}"`;
414
+
415
+ this._dispatcher.dispatchMediaCommand(
416
+ {
417
+ Command: tmpCommand,
418
+ InputPath: tmpRelPath,
419
+ OutputFilename: tmpOutputFilename,
420
+ AffinityKey: tmpRelPath,
421
+ TimeoutMs: 60000
422
+ },
423
+ (pDispatchError, pResult) =>
424
+ {
425
+ if (!pDispatchError && pResult && pResult.OutputBuffer)
426
+ {
427
+ try
428
+ {
429
+ // Write the output buffer to the expected path
430
+ let tmpDir = libPath.dirname(pOutputPath);
431
+ if (!libFs.existsSync(tmpDir))
432
+ {
433
+ libFs.mkdirSync(tmpDir, { recursive: true });
434
+ }
435
+ libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
436
+ return fCallback(null, true);
437
+ }
438
+ catch (pWriteError)
439
+ {
440
+ // Fall through to local
441
+ }
442
+ }
443
+
444
+ // Fall through to local processing
445
+ let tmpSuccess = tmpSelf._extractFrameLocal(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat);
446
+ return fCallback(null, tmpSuccess);
447
+ });
448
+ return;
449
+ }
450
+ }
451
+
452
+ // Local processing
453
+ let tmpSuccess = this._extractFrameLocal(pAbsPath, pTimestamp, pOutputPath, pWidth, pHeight, pFormat);
454
+ return fCallback(null, tmpSuccess);
455
+ }
456
+
457
+ /**
458
+ * Format a timestamp in seconds to ffmpeg's HH:MM:SS.mmm format.
459
+ *
460
+ * @param {number} pTimestamp - Timestamp in seconds
461
+ * @returns {string}
462
+ */
463
+ _formatFfmpegTimestamp(pTimestamp)
464
+ {
465
+ let tmpHours = Math.floor(pTimestamp / 3600);
466
+ let tmpMinutes = Math.floor((pTimestamp % 3600) / 60);
467
+ let tmpSeconds = pTimestamp % 60;
468
+ return `${String(tmpHours).padStart(2, '0')}:${String(tmpMinutes).padStart(2, '0')}:${tmpSeconds.toFixed(3).padStart(6, '0')}`;
469
+ }
470
+
300
471
  /**
301
472
  * Format a timestamp in seconds to a human-readable string.
302
473
  *
@@ -443,66 +614,127 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
443
614
 
444
615
  tmpSelf.fable.log.info(`Extracting ${tmpTimestamps.length} frames from ${pRelPath} (${tmpDuration.toFixed(1)}s)`);
445
616
 
446
- for (let i = 0; i < tmpTimestamps.length; i++)
617
+ // Use async serial extraction when dispatcher is available,
618
+ // otherwise use synchronous loop for backward compatibility
619
+ let tmpUseAsync = !!(tmpSelf._dispatcher && tmpSelf._dispatcher.isAvailable());
620
+
621
+ let _finishExtraction = () =>
447
622
  {
448
- let tmpTimestamp = tmpTimestamps[i];
449
- let tmpFrameFilename = `frame_${String(i).padStart(4, '0')}.${tmpFormat}`;
450
- let tmpFramePath = libPath.join(tmpCacheDir, tmpFrameFilename);
623
+ if (tmpExtractedCount === 0)
624
+ {
625
+ return fCallback(new Error('Failed to extract any frames from the video.'));
626
+ }
451
627
 
452
- let tmpSuccess = tmpSelf._extractFrame(
453
- pAbsPath, tmpTimestamp, tmpFramePath, tmpWidth, tmpHeight, tmpFormat);
628
+ let tmpResult =
629
+ {
630
+ Success: true,
631
+ Path: pRelPath,
632
+ Duration: tmpDuration,
633
+ DurationFormatted: tmpSelf._formatTimestamp(tmpDuration),
634
+ VideoWidth: pVideoInfo.width,
635
+ VideoHeight: pVideoInfo.height,
636
+ Codec: pVideoInfo.codec,
637
+ Bitrate: pVideoInfo.bitrate,
638
+ FileSize: pVideoInfo.size || tmpStat.size,
639
+ FrameCount: tmpExtractedCount,
640
+ FrameWidth: tmpWidth,
641
+ FrameHeight: tmpHeight,
642
+ FrameFormat: tmpFormat,
643
+ CacheKey: libPath.basename(tmpCacheDir),
644
+ Frames: tmpFrames
645
+ };
454
646
 
455
- if (tmpSuccess)
647
+ // Write manifest to cache
648
+ try
456
649
  {
457
- let tmpFrameStat = libFs.statSync(tmpFramePath);
458
- tmpFrames.push(
459
- {
460
- Index: i,
461
- Timestamp: tmpTimestamp,
462
- TimestampFormatted: tmpSelf._formatTimestamp(tmpTimestamp),
463
- Filename: tmpFrameFilename,
464
- Size: tmpFrameStat.size
465
- });
466
- tmpExtractedCount++;
650
+ libFs.writeFileSync(tmpManifestPath, JSON.stringify(tmpResult, null, '\t'));
651
+ }
652
+ catch (pWriteError)
653
+ {
654
+ tmpSelf.fable.log.warn(`Could not write frame manifest: ${pWriteError.message}`);
467
655
  }
468
- }
469
-
470
- if (tmpExtractedCount === 0)
471
- {
472
- return fCallback(new Error('Failed to extract any frames from the video.'));
473
- }
474
656
 
475
- let tmpResult =
476
- {
477
- Success: true,
478
- Path: pRelPath,
479
- Duration: tmpDuration,
480
- DurationFormatted: tmpSelf._formatTimestamp(tmpDuration),
481
- VideoWidth: pVideoInfo.width,
482
- VideoHeight: pVideoInfo.height,
483
- Codec: pVideoInfo.codec,
484
- Bitrate: pVideoInfo.bitrate,
485
- FileSize: pVideoInfo.size || tmpStat.size,
486
- FrameCount: tmpExtractedCount,
487
- FrameWidth: tmpWidth,
488
- FrameHeight: tmpHeight,
489
- FrameFormat: tmpFormat,
490
- CacheKey: libPath.basename(tmpCacheDir),
491
- Frames: tmpFrames
657
+ tmpSelf.fable.log.info(`Extracted ${tmpExtractedCount} frames for ${pRelPath}`);
658
+ return fCallback(null, tmpResult);
492
659
  };
493
660
 
494
- // Write manifest to cache
495
- try
661
+ if (tmpUseAsync)
496
662
  {
497
- libFs.writeFileSync(tmpManifestPath, JSON.stringify(tmpResult, null, '\t'));
663
+ // Serial async extraction
664
+ let tmpFrameIndex = 0;
665
+
666
+ let _extractNext = () =>
667
+ {
668
+ if (tmpFrameIndex >= tmpTimestamps.length)
669
+ {
670
+ return _finishExtraction();
671
+ }
672
+
673
+ let tmpI = tmpFrameIndex;
674
+ let tmpTimestamp = tmpTimestamps[tmpI];
675
+ let tmpFrameFilename = `frame_${String(tmpI).padStart(4, '0')}.${tmpFormat}`;
676
+ let tmpFramePath = libPath.join(tmpCacheDir, tmpFrameFilename);
677
+ tmpFrameIndex++;
678
+
679
+ tmpSelf._extractFrameAsync(
680
+ pAbsPath, tmpTimestamp, tmpFramePath, tmpWidth, tmpHeight, tmpFormat,
681
+ (pExtractError, pSuccess) =>
682
+ {
683
+ if (pSuccess)
684
+ {
685
+ try
686
+ {
687
+ let tmpFrameStat = libFs.statSync(tmpFramePath);
688
+ tmpFrames.push(
689
+ {
690
+ Index: tmpI,
691
+ Timestamp: tmpTimestamp,
692
+ TimestampFormatted: tmpSelf._formatTimestamp(tmpTimestamp),
693
+ Filename: tmpFrameFilename,
694
+ Size: tmpFrameStat.size
695
+ });
696
+ tmpExtractedCount++;
697
+ }
698
+ catch (pStatErr)
699
+ {
700
+ // Frame file disappeared — skip
701
+ }
702
+ }
703
+ _extractNext();
704
+ });
705
+ };
706
+
707
+ _extractNext();
498
708
  }
499
- catch (pWriteError)
709
+ else
500
710
  {
501
- tmpSelf.fable.log.warn(`Could not write frame manifest: ${pWriteError.message}`);
502
- }
711
+ // Synchronous extraction (original behavior)
712
+ for (let i = 0; i < tmpTimestamps.length; i++)
713
+ {
714
+ let tmpTimestamp = tmpTimestamps[i];
715
+ let tmpFrameFilename = `frame_${String(i).padStart(4, '0')}.${tmpFormat}`;
716
+ let tmpFramePath = libPath.join(tmpCacheDir, tmpFrameFilename);
717
+
718
+ let tmpSuccess = tmpSelf._extractFrame(
719
+ pAbsPath, tmpTimestamp, tmpFramePath, tmpWidth, tmpHeight, tmpFormat);
720
+
721
+ if (tmpSuccess)
722
+ {
723
+ let tmpFrameStat = libFs.statSync(tmpFramePath);
724
+ tmpFrames.push(
725
+ {
726
+ Index: i,
727
+ Timestamp: tmpTimestamp,
728
+ TimestampFormatted: tmpSelf._formatTimestamp(tmpTimestamp),
729
+ Filename: tmpFrameFilename,
730
+ Size: tmpFrameStat.size
731
+ });
732
+ tmpExtractedCount++;
733
+ }
734
+ }
503
735
 
504
- tmpSelf.fable.log.info(`Extracted ${tmpExtractedCount} frames for ${pRelPath}`);
505
- return fCallback(null, tmpResult);
736
+ _finishExtraction();
737
+ }
506
738
  });
507
739
  }
508
740
 
@@ -59,6 +59,8 @@ class RetoldRemoteAudioExplorerView extends libPictView
59
59
  {
60
60
  let tmpRemote = this.pict.AppData.RetoldRemote;
61
61
  tmpRemote.ActiveMode = 'audio-explorer';
62
+ tmpRemote.CurrentViewerFile = pFilePath;
63
+ tmpRemote.CurrentViewerMediaType = 'audio';
62
64
  this._currentPath = pFilePath;
63
65
  this._waveformData = null;
64
66
  this._peaks = [];
@@ -102,8 +104,12 @@ class RetoldRemoteAudioExplorerView extends libPictView
102
104
 
103
105
  // Header
104
106
  tmpHTML += '<div class="retold-remote-aex-header">';
105
- tmpHTML += '<button class="retold-remote-aex-nav-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].goBack()" title="Back to audio (Esc)">&larr; Back</button>';
107
+ tmpHTML += '<button class="retold-remote-aex-nav-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].goBack()" title="Back (Esc)">&larr; Back</button>';
106
108
  tmpHTML += '<div class="retold-remote-aex-title">Audio Explorer &mdash; ' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFileName) + '</div>';
109
+ tmpHTML += '<div class="retold-remote-aex-actions">';
110
+ tmpHTML += '<button class="retold-remote-aex-action-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].playInBrowser()" title="Play in browser">&#9654; Play</button>';
111
+ tmpHTML += '<button class="retold-remote-aex-action-btn" onclick="pict.providers[\'RetoldRemote-GalleryNavigation\']._streamWithVLC()" title="Stream with VLC (v)">&#9654; VLC</button>';
112
+ tmpHTML += '</div>';
107
113
  tmpHTML += '</div>';
108
114
 
109
115
  // Info bar (populated after waveform loads)
@@ -971,7 +977,7 @@ class RetoldRemoteAudioExplorerView extends libPictView
971
977
  }
972
978
 
973
979
  /**
974
- * Navigate back to the audio player viewer.
980
+ * Navigate back to the gallery / file listing.
975
981
  */
976
982
  goBack()
977
983
  {
@@ -982,21 +988,29 @@ class RetoldRemoteAudioExplorerView extends libPictView
982
988
  this._resizeObserver = null;
983
989
  }
984
990
 
985
- if (this._currentPath)
991
+ let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
992
+ if (tmpNav)
986
993
  {
987
- let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
988
- if (tmpViewer)
989
- {
990
- tmpViewer.showMedia(this._currentPath, 'audio');
991
- }
994
+ tmpNav.closeViewer();
992
995
  }
993
- else
996
+ }
997
+
998
+ /**
999
+ * Leave the audio explorer and play the file in the browser viewer.
1000
+ */
1001
+ playInBrowser()
1002
+ {
1003
+ // Clean up resize observer
1004
+ if (this._resizeObserver)
994
1005
  {
995
- let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
996
- if (tmpNav)
997
- {
998
- tmpNav.closeViewer();
999
- }
1006
+ this._resizeObserver.disconnect();
1007
+ this._resizeObserver = null;
1008
+ }
1009
+
1010
+ let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
1011
+ if (tmpViewer)
1012
+ {
1013
+ tmpViewer.showMedia(this._currentPath, 'audio');
1000
1014
  }
1001
1015
  }
1002
1016
 
@@ -34,6 +34,8 @@ class RetoldRemoteImageExplorerView extends libPictView
34
34
  {
35
35
  let tmpRemote = this.pict.AppData.RetoldRemote;
36
36
  tmpRemote.ActiveMode = 'image-explorer';
37
+ tmpRemote.CurrentViewerFile = pFilePath;
38
+ tmpRemote.CurrentViewerMediaType = 'image';
37
39
  this._currentPath = pFilePath;
38
40
  this._dziData = null;
39
41
  this._loading = false;
@@ -86,6 +88,9 @@ class RetoldRemoteImageExplorerView extends libPictView
86
88
  tmpHTML += '<div class="retold-remote-iex-header">';
87
89
  tmpHTML += '<button class="retold-remote-iex-nav-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].goBack()" title="Back (Esc)">&larr; Back</button>';
88
90
  tmpHTML += '<div class="retold-remote-iex-title">Image Explorer &mdash; ' + tmpFmt.escapeHTML(tmpFileName) + '</div>';
91
+ tmpHTML += '<div class="retold-remote-iex-actions">';
92
+ tmpHTML += '<button class="retold-remote-iex-action-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].viewInBrowser()" title="View in standard viewer">&#128444; View</button>';
93
+ tmpHTML += '</div>';
89
94
  tmpHTML += '</div>';
90
95
 
91
96
  // Info bar
@@ -732,7 +737,7 @@ class RetoldRemoteImageExplorerView extends libPictView
732
737
  }
733
738
 
734
739
  /**
735
- * Navigate back to the image viewer.
740
+ * Navigate back to the gallery / file listing.
736
741
  */
737
742
  goBack()
738
743
  {
@@ -750,21 +755,36 @@ class RetoldRemoteImageExplorerView extends libPictView
750
755
  this._osdViewer = null;
751
756
  }
752
757
 
753
- if (this._currentPath)
758
+ let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
759
+ if (tmpNav)
754
760
  {
755
- let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
756
- if (tmpViewer)
757
- {
758
- tmpViewer.showMedia(this._currentPath, 'image');
759
- }
761
+ tmpNav.closeViewer();
760
762
  }
761
- else
763
+ }
764
+
765
+ /**
766
+ * Leave the image explorer and view the image in the standard viewer.
767
+ */
768
+ viewInBrowser()
769
+ {
770
+ // Destroy the OSD viewer
771
+ if (this._osdViewer)
762
772
  {
763
- let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
764
- if (tmpNav)
773
+ try
774
+ {
775
+ this._osdViewer.destroy();
776
+ }
777
+ catch (pErr)
765
778
  {
766
- tmpNav.closeViewer();
779
+ // ignore
767
780
  }
781
+ this._osdViewer = null;
782
+ }
783
+
784
+ let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
785
+ if (tmpViewer)
786
+ {
787
+ tmpViewer.showMedia(this._currentPath, 'image');
768
788
  }
769
789
  }
770
790