retold-remote 0.0.5 → 0.0.7

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 (71) hide show
  1. package/css/retold-remote.css +3 -0
  2. package/docs/README.md +181 -0
  3. package/docs/_cover.md +14 -0
  4. package/docs/_sidebar.md +10 -0
  5. package/docs/_topbar.md +3 -0
  6. package/docs/audio-viewer.md +133 -0
  7. package/docs/ebook-reader.md +90 -0
  8. package/docs/image-viewer.md +90 -0
  9. package/docs/server-setup.md +262 -0
  10. package/docs/video-viewer.md +134 -0
  11. package/html/docs.html +59 -0
  12. package/html/index.html +1 -1
  13. package/package.json +20 -6
  14. package/source/Pict-Application-RetoldRemote.js +122 -2
  15. package/source/RetoldRemote-ExtensionMaps.js +33 -0
  16. package/source/cli/RetoldRemote-Server-Setup.js +211 -67
  17. package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
  18. package/source/providers/Pict-Provider-AISortManager.js +456 -0
  19. package/source/providers/Pict-Provider-CollectionManager.js +1200 -0
  20. package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
  21. package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
  22. package/source/providers/Pict-Provider-GalleryNavigation.js +187 -409
  23. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
  24. package/source/providers/Pict-Provider-ToastNotification.js +96 -0
  25. package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
  26. package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
  27. package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
  28. package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
  29. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
  30. package/source/server/RetoldRemote-AISortService.js +879 -0
  31. package/source/server/RetoldRemote-ArchiveService.js +2 -12
  32. package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
  33. package/source/server/RetoldRemote-CollectionService.js +843 -0
  34. package/source/server/RetoldRemote-EbookService.js +7 -16
  35. package/source/server/RetoldRemote-FileOperationService.js +560 -0
  36. package/source/server/RetoldRemote-MediaService.js +15 -14
  37. package/source/server/RetoldRemote-MetadataCache.js +411 -0
  38. package/source/server/RetoldRemote-ParimeCache.js +349 -0
  39. package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
  40. package/source/server/RetoldRemote-VideoFrameService.js +7 -15
  41. package/source/views/PictView-Remote-AudioExplorer.js +10 -43
  42. package/source/views/PictView-Remote-CollectionsPanel.js +1486 -0
  43. package/source/views/PictView-Remote-Gallery.js +237 -44
  44. package/source/views/PictView-Remote-ImageViewer.js +1 -34
  45. package/source/views/PictView-Remote-Layout.js +411 -20
  46. package/source/views/PictView-Remote-MediaViewer.js +311 -28
  47. package/source/views/PictView-Remote-SettingsPanel.js +275 -133
  48. package/source/views/PictView-Remote-TopBar.js +701 -14
  49. package/source/views/PictView-Remote-VLCSetup.js +8 -39
  50. package/source/views/PictView-Remote-VideoExplorer.js +20 -54
  51. package/web-application/css/docuserve.css +73 -0
  52. package/web-application/css/retold-remote.css +3 -0
  53. package/web-application/docs/README.md +181 -0
  54. package/web-application/docs/_cover.md +14 -0
  55. package/web-application/docs/_sidebar.md +10 -0
  56. package/web-application/docs/_topbar.md +3 -0
  57. package/web-application/docs/audio-viewer.md +133 -0
  58. package/web-application/docs/ebook-reader.md +90 -0
  59. package/web-application/docs/image-viewer.md +90 -0
  60. package/web-application/docs/server-setup.md +262 -0
  61. package/web-application/docs/video-viewer.md +134 -0
  62. package/web-application/docs.html +59 -0
  63. package/web-application/index.html +1 -1
  64. package/web-application/js/pict-docuserve.min.js +58 -0
  65. package/web-application/js/pict.min.js +2 -2
  66. package/web-application/js/pict.min.js.map +1 -1
  67. package/web-application/retold-remote.js +2652 -970
  68. package/web-application/retold-remote.js.map +1 -1
  69. package/web-application/retold-remote.min.js +60 -13
  70. package/web-application/retold-remote.min.js.map +1 -1
  71. package/server.js +0 -43
@@ -233,7 +233,7 @@ const _ViewConfiguration =
233
233
  font-style: italic;
234
234
  margin-top: 4px;
235
235
  }
236
- .retold-remote-vlc-setup-toast
236
+ .retold-remote-toast
237
237
  {
238
238
  position: fixed;
239
239
  bottom: 20px;
@@ -445,7 +445,7 @@ class RetoldRemoteVLCSetupView extends libPictView
445
445
  tmpHTML += '</div>';
446
446
 
447
447
  let tmpScript = this._getMacSetupScript();
448
- tmpHTML += '<div class="retold-remote-vlc-setup-code">' + this._escapeHTML(tmpScript) + '</div>';
448
+ tmpHTML += '<div class="retold-remote-vlc-setup-code">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpScript) + '</div>';
449
449
  tmpHTML += '<button class="retold-remote-vlc-setup-btn primary" onclick="pict.views[\'RetoldRemote-VLCSetup\'].copyMacSetup()">Copy to Clipboard</button>';
450
450
  tmpHTML += '</div>';
451
451
 
@@ -509,7 +509,7 @@ class RetoldRemoteVLCSetupView extends libPictView
509
509
  tmpHTML += '</div>';
510
510
 
511
511
  let tmpRegFile = this._getWindowsRegFile();
512
- tmpHTML += '<div class="retold-remote-vlc-setup-code">' + this._escapeHTML(tmpRegFile) + '</div>';
512
+ tmpHTML += '<div class="retold-remote-vlc-setup-code">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpRegFile) + '</div>';
513
513
  tmpHTML += '<button class="retold-remote-vlc-setup-btn primary" onclick="pict.views[\'RetoldRemote-VLCSetup\'].copyWindowsReg()">Copy to Clipboard</button>';
514
514
  tmpHTML += '</div>';
515
515
 
@@ -521,7 +521,7 @@ class RetoldRemoteVLCSetupView extends libPictView
521
521
  tmpHTML += '</div>';
522
522
 
523
523
  let tmpBatchScript = this._getWindowsBatchScript();
524
- tmpHTML += '<div class="retold-remote-vlc-setup-code">' + this._escapeHTML(tmpBatchScript) + '</div>';
524
+ tmpHTML += '<div class="retold-remote-vlc-setup-code">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpBatchScript) + '</div>';
525
525
  tmpHTML += '<button class="retold-remote-vlc-setup-btn primary" onclick="pict.views[\'RetoldRemote-VLCSetup\'].copyWindowsBatch()">Copy to Clipboard</button>';
526
526
  tmpHTML += '</div>';
527
527
 
@@ -546,7 +546,7 @@ class RetoldRemoteVLCSetupView extends libPictView
546
546
  tmpHTML += '<div class="retold-remote-vlc-setup-section-title">Setup Command</div>';
547
547
 
548
548
  let tmpScript = this._getLinuxSetupScript();
549
- tmpHTML += '<div class="retold-remote-vlc-setup-code">' + this._escapeHTML(tmpScript) + '</div>';
549
+ tmpHTML += '<div class="retold-remote-vlc-setup-code">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpScript) + '</div>';
550
550
  tmpHTML += '<button class="retold-remote-vlc-setup-btn primary" onclick="pict.views[\'RetoldRemote-VLCSetup\'].copyLinuxSetup()">Copy to Clipboard</button>';
551
551
  tmpHTML += '</div>';
552
552
 
@@ -687,22 +687,13 @@ class RetoldRemoteVLCSetupView extends libPictView
687
687
  ].join('\n');
688
688
  }
689
689
 
690
- _escapeHTML(pStr)
691
- {
692
- return pStr
693
- .replace(/&/g, '&amp;')
694
- .replace(/</g, '&lt;')
695
- .replace(/>/g, '&gt;')
696
- .replace(/"/g, '&quot;');
697
- }
698
-
699
690
  _copyToClipboard(pText, pLabel)
700
691
  {
701
692
  if (navigator.clipboard && navigator.clipboard.writeText)
702
693
  {
703
694
  navigator.clipboard.writeText(pText).then(() =>
704
695
  {
705
- this._showToast(pLabel + ' copied to clipboard');
696
+ this.pict.providers['RetoldRemote-ToastNotification'].showToast(pLabel + ' copied to clipboard');
706
697
  }).catch(() =>
707
698
  {
708
699
  this._fallbackCopy(pText, pLabel);
@@ -725,37 +716,15 @@ class RetoldRemoteVLCSetupView extends libPictView
725
716
  try
726
717
  {
727
718
  document.execCommand('copy');
728
- this._showToast(pLabel + ' copied to clipboard');
719
+ this.pict.providers['RetoldRemote-ToastNotification'].showToast(pLabel + ' copied to clipboard');
729
720
  }
730
721
  catch (pErr)
731
722
  {
732
- this._showToast('Failed to copy - please select and copy manually');
723
+ this.pict.providers['RetoldRemote-ToastNotification'].showToast('Failed to copy - please select and copy manually');
733
724
  }
734
725
  document.body.removeChild(tmpTextarea);
735
726
  }
736
727
 
737
- _showToast(pMessage)
738
- {
739
- let tmpExisting = document.querySelector('.retold-remote-vlc-setup-toast');
740
- if (tmpExisting)
741
- {
742
- tmpExisting.remove();
743
- }
744
-
745
- let tmpToast = document.createElement('div');
746
- tmpToast.className = 'retold-remote-vlc-setup-toast';
747
- tmpToast.textContent = pMessage;
748
- document.body.appendChild(tmpToast);
749
-
750
- setTimeout(() =>
751
- {
752
- if (tmpToast.parentNode)
753
- {
754
- tmpToast.remove();
755
- }
756
- }, 2000);
757
- }
758
-
759
728
  copyMacSetup()
760
729
  {
761
730
  this._copyToClipboard(this._getMacSetupScript(), 'macOS setup script');
@@ -400,7 +400,7 @@ class RetoldRemoteVideoExplorerView extends libPictView
400
400
  // Header
401
401
  tmpHTML += '<div class="retold-remote-vex-header">';
402
402
  tmpHTML += '<button class="retold-remote-vex-nav-btn" onclick="pict.views[\'RetoldRemote-VideoExplorer\'].goBack()" title="Back to video (Esc)">&larr; Back</button>';
403
- tmpHTML += '<div class="retold-remote-vex-title">Video Explorer &mdash; ' + this._escapeHTML(tmpFileName) + '</div>';
403
+ tmpHTML += '<div class="retold-remote-vex-title">Video Explorer &mdash; ' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFileName) + '</div>';
404
404
  tmpHTML += '</div>';
405
405
 
406
406
  // Info bar (populated after frames load)
@@ -503,18 +503,18 @@ class RetoldRemoteVideoExplorerView extends libPictView
503
503
  if (tmpInfoBar)
504
504
  {
505
505
  let tmpInfoHTML = '';
506
- tmpInfoHTML += '<span class="retold-remote-vex-info-item"><span class="retold-remote-vex-info-label">Duration</span> <span class="retold-remote-vex-info-value">' + this._escapeHTML(tmpData.DurationFormatted) + '</span></span>';
506
+ tmpInfoHTML += '<span class="retold-remote-vex-info-item"><span class="retold-remote-vex-info-label">Duration</span> <span class="retold-remote-vex-info-value">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpData.DurationFormatted) + '</span></span>';
507
507
  if (tmpData.VideoWidth && tmpData.VideoHeight)
508
508
  {
509
509
  tmpInfoHTML += '<span class="retold-remote-vex-info-item"><span class="retold-remote-vex-info-label">Resolution</span> <span class="retold-remote-vex-info-value">' + tmpData.VideoWidth + '&times;' + tmpData.VideoHeight + '</span></span>';
510
510
  }
511
511
  if (tmpData.Codec)
512
512
  {
513
- tmpInfoHTML += '<span class="retold-remote-vex-info-item"><span class="retold-remote-vex-info-label">Codec</span> <span class="retold-remote-vex-info-value">' + this._escapeHTML(tmpData.Codec) + '</span></span>';
513
+ tmpInfoHTML += '<span class="retold-remote-vex-info-item"><span class="retold-remote-vex-info-label">Codec</span> <span class="retold-remote-vex-info-value">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpData.Codec) + '</span></span>';
514
514
  }
515
515
  if (tmpData.FileSize)
516
516
  {
517
- tmpInfoHTML += '<span class="retold-remote-vex-info-item"><span class="retold-remote-vex-info-label">Size</span> <span class="retold-remote-vex-info-value">' + this._formatFileSize(tmpData.FileSize) + '</span></span>';
517
+ tmpInfoHTML += '<span class="retold-remote-vex-info-item"><span class="retold-remote-vex-info-label">Size</span> <span class="retold-remote-vex-info-value">' + this.pict.providers['RetoldRemote-FormattingUtilities'].formatFileSize(tmpData.FileSize) + '</span></span>';
518
518
  }
519
519
  tmpInfoHTML += '<span class="retold-remote-vex-info-item"><span class="retold-remote-vex-info-label">Frames</span> <span class="retold-remote-vex-info-value">' + tmpData.FrameCount + '</span></span>';
520
520
 
@@ -541,9 +541,9 @@ class RetoldRemoteVideoExplorerView extends libPictView
541
541
  let tmpFrameURL = '/api/media/video-frame/' + tmpData.CacheKey + '/' + tmpFrame.Filename;
542
542
 
543
543
  tmpGridHTML += '<div class="retold-remote-vex-frame" id="retold-vex-frame-' + i + '" onclick="pict.views[\'RetoldRemote-VideoExplorer\'].selectFrame(' + i + ')" ondblclick="pict.views[\'RetoldRemote-VideoExplorer\'].openFrameFullsize(' + i + ')">';
544
- tmpGridHTML += '<img src="' + tmpFrameURL + '" alt="Frame at ' + this._escapeHTML(tmpFrame.TimestampFormatted) + '" loading="lazy">';
544
+ tmpGridHTML += '<img src="' + tmpFrameURL + '" alt="Frame at ' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFrame.TimestampFormatted) + '" loading="lazy">';
545
545
  tmpGridHTML += '<div class="retold-remote-vex-frame-info">';
546
- tmpGridHTML += '<span class="retold-remote-vex-frame-timestamp">' + this._escapeHTML(tmpFrame.TimestampFormatted) + '</span>';
546
+ tmpGridHTML += '<span class="retold-remote-vex-frame-timestamp">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFrame.TimestampFormatted) + '</span>';
547
547
  tmpGridHTML += '<span class="retold-remote-vex-frame-index">#' + (tmpFrame.Index + 1) + '</span>';
548
548
  tmpGridHTML += '</div>';
549
549
  tmpGridHTML += '</div>';
@@ -586,7 +586,7 @@ class RetoldRemoteVideoExplorerView extends libPictView
586
586
  let tmpSelectedClass = (i === this._selectedFrameIndex) ? ' selected' : '';
587
587
  tmpHTML += '<div class="retold-remote-vex-timeline-marker' + tmpSelectedClass + '" '
588
588
  + 'style="left:' + tmpPercent.toFixed(2) + '%;" '
589
- + 'title="' + this._escapeHTML(tmpFrame.TimestampFormatted) + '" '
589
+ + 'title="' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFrame.TimestampFormatted) + '" '
590
590
  + 'onclick="event.stopPropagation(); pict.views[\'RetoldRemote-VideoExplorer\'].selectFrame(' + i + ')">'
591
591
  + '</div>';
592
592
  }
@@ -600,13 +600,13 @@ class RetoldRemoteVideoExplorerView extends libPictView
600
600
  let tmpPercent = (tmpCustom.Timestamp / tmpData.Duration) * 100;
601
601
  tmpHTML += '<div class="retold-remote-vex-timeline-marker custom" '
602
602
  + 'style="left:' + tmpPercent.toFixed(2) + '%;" '
603
- + 'title="' + this._escapeHTML(tmpCustom.TimestampFormatted) + '">'
603
+ + 'title="' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpCustom.TimestampFormatted) + '">'
604
604
  + '</div>';
605
605
  }
606
606
  }
607
607
 
608
608
  tmpHTML += '</div>';
609
- tmpHTML += '<span class="retold-remote-vex-timeline-label">' + this._escapeHTML(tmpData.DurationFormatted) + '</span>';
609
+ tmpHTML += '<span class="retold-remote-vex-timeline-label">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpData.DurationFormatted) + '</span>';
610
610
 
611
611
  tmpTimeline.innerHTML = tmpHTML;
612
612
  tmpTimeline.style.display = '';
@@ -729,13 +729,13 @@ class RetoldRemoteVideoExplorerView extends libPictView
729
729
  if (tmpPlaceholder)
730
730
  {
731
731
  let tmpFrameURL = '/api/media/video-frame/' + tmpData.CacheKey + '/' + pResult.Filename;
732
- let tmpEscFilename = tmpSelf._escapeHTML(pResult.Filename).replace(/'/g, "\\'");
733
- let tmpEscTimestamp = tmpSelf._escapeHTML(pResult.TimestampFormatted).replace(/'/g, "\\'");
732
+ let tmpEscFilename = tmpSelf.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pResult.Filename).replace(/'/g, "\\'");
733
+ let tmpEscTimestamp = tmpSelf.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pResult.TimestampFormatted).replace(/'/g, "\\'");
734
734
  tmpPlaceholder.ondblclick = function() { pict.views['RetoldRemote-VideoExplorer'].openCustomFrameFullsize(tmpEscFilename, tmpEscTimestamp); };
735
735
  tmpPlaceholder.style.cursor = 'pointer';
736
- tmpPlaceholder.innerHTML = '<img src="' + tmpFrameURL + '" alt="Frame at ' + tmpSelf._escapeHTML(pResult.TimestampFormatted) + '" loading="lazy">'
736
+ tmpPlaceholder.innerHTML = '<img src="' + tmpFrameURL + '" alt="Frame at ' + tmpSelf.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pResult.TimestampFormatted) + '" loading="lazy">'
737
737
  + '<div class="retold-remote-vex-frame-info">'
738
- + '<span class="retold-remote-vex-frame-timestamp">' + tmpSelf._escapeHTML(pResult.TimestampFormatted) + '</span>'
738
+ + '<span class="retold-remote-vex-frame-timestamp">' + tmpSelf.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pResult.TimestampFormatted) + '</span>'
739
739
  + '<span class="retold-remote-vex-frame-index">custom</span>'
740
740
  + '</div>';
741
741
  }
@@ -748,9 +748,9 @@ class RetoldRemoteVideoExplorerView extends libPictView
748
748
  let tmpPlaceholder = document.getElementById(tmpPlaceholderId);
749
749
  if (tmpPlaceholder)
750
750
  {
751
- tmpPlaceholder.innerHTML = '<div class="retold-remote-vex-frame-loading">Failed: ' + tmpSelf._escapeHTML(pError.message) + '</div>'
751
+ tmpPlaceholder.innerHTML = '<div class="retold-remote-vex-frame-loading">Failed: ' + tmpSelf.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pError.message) + '</div>'
752
752
  + '<div class="retold-remote-vex-frame-info">'
753
- + '<span class="retold-remote-vex-frame-timestamp">' + tmpSelf._formatTimestamp(tmpTimestamp) + '</span>'
753
+ + '<span class="retold-remote-vex-frame-timestamp">' + tmpSelf.pict.providers['RetoldRemote-FormattingUtilities'].formatTimestamp(tmpTimestamp) + '</span>'
754
754
  + '<span class="retold-remote-vex-frame-index">error</span>'
755
755
  + '</div>';
756
756
  }
@@ -780,7 +780,7 @@ class RetoldRemoteVideoExplorerView extends libPictView
780
780
  tmpEl.id = pPlaceholderId;
781
781
  tmpEl.innerHTML = '<div class="retold-remote-vex-frame-loading">Extracting...</div>'
782
782
  + '<div class="retold-remote-vex-frame-info">'
783
- + '<span class="retold-remote-vex-frame-timestamp">' + this._formatTimestamp(pTimestamp) + '</span>'
783
+ + '<span class="retold-remote-vex-frame-timestamp">' + this.pict.providers['RetoldRemote-FormattingUtilities'].formatTimestamp(pTimestamp) + '</span>'
784
784
  + '<span class="retold-remote-vex-frame-index">custom</span>'
785
785
  + '</div>';
786
786
 
@@ -840,25 +840,6 @@ class RetoldRemoteVideoExplorerView extends libPictView
840
840
  return parseFloat(pText) || 0;
841
841
  }
842
842
 
843
- /**
844
- * Format a timestamp in seconds to a human-readable string.
845
- *
846
- * @param {number} pSeconds - Timestamp in seconds
847
- * @returns {string} Formatted string like "1:23" or "1:02:34"
848
- */
849
- _formatTimestamp(pSeconds)
850
- {
851
- let tmpHours = Math.floor(pSeconds / 3600);
852
- let tmpMinutes = Math.floor((pSeconds % 3600) / 60);
853
- let tmpSecs = Math.floor(pSeconds % 60);
854
-
855
- if (tmpHours > 0)
856
- {
857
- return tmpHours + ':' + String(tmpMinutes).padStart(2, '0') + ':' + String(tmpSecs).padStart(2, '0');
858
- }
859
- return tmpMinutes + ':' + String(tmpSecs).padStart(2, '0');
860
- }
861
-
862
843
  /**
863
844
  * Open a frame at full size in an inline preview overlay.
864
845
  *
@@ -1000,11 +981,11 @@ class RetoldRemoteVideoExplorerView extends libPictView
1000
981
  tmpHTML += '<div class="retold-remote-vex-preview-header">';
1001
982
  tmpHTML += '<button class="retold-remote-vex-nav-btn" onclick="pict.views[\'RetoldRemote-VideoExplorer\'].closeFramePreview()" title="Back (Esc)">&larr; Back</button>';
1002
983
  tmpHTML += '<button class="retold-remote-vex-nav-btn" onclick="pict.views[\'RetoldRemote-VideoExplorer\'].previewPrevFrame()" title="Previous (\u2190)">&lsaquo; Prev</button>';
1003
- tmpHTML += '<div class="retold-remote-vex-preview-title" id="RetoldRemote-VEX-PreviewTitle">' + this._escapeHTML(pLabel) + '</div>';
984
+ tmpHTML += '<div class="retold-remote-vex-preview-title" id="RetoldRemote-VEX-PreviewTitle">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pLabel) + '</div>';
1004
985
  tmpHTML += '<button class="retold-remote-vex-nav-btn" onclick="pict.views[\'RetoldRemote-VideoExplorer\'].previewNextFrame()" title="Next (\u2192)">Next &rsaquo;</button>';
1005
986
  tmpHTML += '</div>';
1006
987
  tmpHTML += '<div class="retold-remote-vex-preview-body" id="RetoldRemote-VEX-PreviewBody">';
1007
- tmpHTML += '<img src="' + pURL + '" alt="' + this._escapeHTML(pLabel) + '">';
988
+ tmpHTML += '<img src="' + pURL + '" alt="' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pLabel) + '">';
1008
989
  tmpHTML += '</div>';
1009
990
 
1010
991
  tmpBackdrop.innerHTML = tmpHTML;
@@ -1099,7 +1080,7 @@ class RetoldRemoteVideoExplorerView extends libPictView
1099
1080
  let tmpBody = document.getElementById('RetoldRemote-VEX-PreviewBody');
1100
1081
  if (tmpBody)
1101
1082
  {
1102
- tmpBody.innerHTML = '<img src="' + tmpURL + '" alt="' + this._escapeHTML(tmpFrame.Label) + '">';
1083
+ tmpBody.innerHTML = '<img src="' + tmpURL + '" alt="' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(tmpFrame.Label) + '">';
1103
1084
  }
1104
1085
 
1105
1086
  let tmpTitle = document.getElementById('RetoldRemote-VEX-PreviewTitle');
@@ -1201,27 +1182,12 @@ class RetoldRemoteVideoExplorerView extends libPictView
1201
1182
  if (tmpBody)
1202
1183
  {
1203
1184
  tmpBody.innerHTML = '<div class="retold-remote-vex-error">'
1204
- + '<div class="retold-remote-vex-error-message">' + this._escapeHTML(pMessage || 'An error occurred.') + '</div>'
1185
+ + '<div class="retold-remote-vex-error-message">' + this.pict.providers['RetoldRemote-FormattingUtilities'].escapeHTML(pMessage || 'An error occurred.') + '</div>'
1205
1186
  + '<button class="retold-remote-vex-nav-btn" onclick="pict.views[\'RetoldRemote-VideoExplorer\'].goBack()">Back to Video</button>'
1206
1187
  + '</div>';
1207
1188
  }
1208
1189
  }
1209
1190
 
1210
- _escapeHTML(pText)
1211
- {
1212
- if (!pText) return '';
1213
- return pText.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1214
- }
1215
-
1216
- _formatFileSize(pBytes)
1217
- {
1218
- if (!pBytes || pBytes === 0) return '0 B';
1219
- let tmpUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
1220
- let tmpIndex = Math.floor(Math.log(pBytes) / Math.log(1024));
1221
- if (tmpIndex >= tmpUnits.length) tmpIndex = tmpUnits.length - 1;
1222
- let tmpSize = pBytes / Math.pow(1024, tmpIndex);
1223
- return tmpSize.toFixed(tmpIndex === 0 ? 0 : 1) + ' ' + tmpUnits[tmpIndex];
1224
- }
1225
1191
  }
1226
1192
 
1227
1193
  RetoldRemoteVideoExplorerView.default_configuration = _ViewConfiguration;
@@ -0,0 +1,73 @@
1
+ /* ============================================================================
2
+ Pict Docuserve - Base Styles
3
+ ============================================================================ */
4
+
5
+ /* Reset and base */
6
+ *, *::before, *::after {
7
+ box-sizing: border-box;
8
+ }
9
+
10
+ html, body {
11
+ margin: 0;
12
+ padding: 0;
13
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
14
+ font-size: 16px;
15
+ line-height: 1.5;
16
+ color: #423D37;
17
+ background-color: #fff;
18
+ -webkit-font-smoothing: antialiased;
19
+ -moz-osx-font-smoothing: grayscale;
20
+ }
21
+
22
+ /* Typography */
23
+ h1, h2, h3, h4, h5, h6 {
24
+ margin-top: 0;
25
+ line-height: 1.3;
26
+ }
27
+
28
+ a {
29
+ color: #2E7D74;
30
+ text-decoration: none;
31
+ }
32
+
33
+ a:hover {
34
+ color: #256861;
35
+ }
36
+
37
+ /* Application container */
38
+ #Docuserve-Application-Container {
39
+ min-height: 100vh;
40
+ }
41
+
42
+ /* Utility: scrollbar styling */
43
+ ::-webkit-scrollbar {
44
+ width: 8px;
45
+ }
46
+
47
+ ::-webkit-scrollbar-track {
48
+ background: #F5F0E8;
49
+ }
50
+
51
+ ::-webkit-scrollbar-thumb {
52
+ background: #D4CCBE;
53
+ border-radius: 4px;
54
+ }
55
+
56
+ ::-webkit-scrollbar-thumb:hover {
57
+ background: #B5AA9A;
58
+ }
59
+
60
+ /* Responsive adjustments */
61
+ @media (max-width: 768px) {
62
+ html {
63
+ font-size: 14px;
64
+ }
65
+
66
+ #Docuserve-Sidebar-Container {
67
+ display: none;
68
+ }
69
+
70
+ .docuserve-body {
71
+ flex-direction: column;
72
+ }
73
+ }
@@ -12,6 +12,7 @@ html, body
12
12
  margin: 0;
13
13
  padding: 0;
14
14
  height: 100%;
15
+ height: 100dvh;
15
16
  overflow: hidden;
16
17
  background: var(--retold-bg-primary);
17
18
  color: var(--retold-text-primary);
@@ -20,6 +21,8 @@ html, body
20
21
  line-height: 1.5;
21
22
  -webkit-font-smoothing: antialiased;
22
23
  -moz-osx-font-smoothing: grayscale;
24
+ /* Safe area insets for notched devices */
25
+ padding-bottom: env(safe-area-inset-bottom);
23
26
  }
24
27
 
25
28
  /* Scrollbar styling */
@@ -0,0 +1,181 @@
1
+ # Retold Remote
2
+
3
+ A browser-based media server and NAS file explorer. Point it at a folder and browse images, videos, audio, ebooks, code, and documents through a keyboard-driven gallery interface with 15 built-in themes.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx retold-remote serve /path/to/media
9
+ ```
10
+
11
+ Or with Docker:
12
+
13
+ ```bash
14
+ docker build -t retold-remote .
15
+ docker run -p 8086:8086 -v /path/to/media:/media retold-remote
16
+ ```
17
+
18
+ Then open `http://localhost:8086` in a browser.
19
+
20
+ ## Features
21
+
22
+ - **Gallery browser** with grid and list views, thumbnail generation, and lazy loading
23
+ - **Image viewer** with three fit modes, 0.25x-8x zoom, and EXIF orientation support
24
+ - **Video viewer** with action menu, in-browser playback, VLC streaming, and frame explorer
25
+ - **Audio viewer** with waveform visualization, selection-based playback, and segment extraction
26
+ - **eBook reader** for EPUB and MOBI with table of contents and page navigation
27
+ - **Code/text viewer** with syntax highlighting for 30+ languages
28
+ - **PDF viewer** via native browser rendering
29
+ - **Archive browsing** into zip, 7z, rar, tar.gz, cbz, and cbr files as virtual folders
30
+ - **Filtering and sorting** by media type, extension, file size, date, and text search (with regex)
31
+ - **15 themes** from greyscale to retro and cyberpunk
32
+ - **Full keyboard navigation** across every mode
33
+ - **Media type override** to force any file open as image, video, audio, or text (keys 1-4)
34
+ - **Hashed filenames** mode to hide real paths from the browser
35
+ - **VLC protocol handler** setup for macOS, Windows, and Linux
36
+ - **Settings persistence** via localStorage
37
+
38
+ ## Documentation
39
+
40
+ | Document | Contents |
41
+ |----------|----------|
42
+ | [Server Setup and Docker](server-setup.md) | Installation, CLI options, environment variables, configuration, Docker |
43
+ | [Image Viewer](image-viewer.md) | Fit modes, zoom, keyboard shortcuts, mouse interactions |
44
+ | [Video Viewer](video-viewer.md) | Action menu, playback, frame explorer, timeline, VLC streaming |
45
+ | [Audio Viewer](audio-viewer.md) | Waveform visualization, selection, zoom, segment playback |
46
+ | [eBook Reader](ebook-reader.md) | EPUB/MOBI support, table of contents, page navigation |
47
+
48
+ ## Keyboard Shortcuts (All Modes)
49
+
50
+ ### Global
51
+
52
+ | Key | Action |
53
+ |-----|--------|
54
+ | `F1` | Help panel |
55
+ | `F9` | Focus sidebar file list |
56
+ | `/` | Search / filter bar |
57
+ | `Esc` | Close overlay / go back |
58
+
59
+ ### Gallery
60
+
61
+ | Key | Action |
62
+ |-----|--------|
63
+ | Arrow keys | Navigate items |
64
+ | `Enter` | Open selected item |
65
+ | `1` `2` `3` `4` | Open as image / video / audio / text |
66
+ | `Esc` | Go up one folder |
67
+ | `Home` / `End` | Jump to first / last item |
68
+ | `g` | Toggle grid / list view |
69
+ | `f` | Advanced filter panel |
70
+ | `s` | Focus sort dropdown |
71
+ | `x` | Clear all filters |
72
+ | `c` | Settings panel |
73
+ | `d` | Distraction-free mode |
74
+
75
+ ### Media Viewer
76
+
77
+ | Key | Action |
78
+ |-----|--------|
79
+ | `Esc` | Back to gallery |
80
+ | Right / `j` | Next file |
81
+ | Left / `k` | Previous file |
82
+ | `1` `2` `3` `4` | View as image / video / audio / text |
83
+ | `Space` | Play / pause |
84
+ | `f` | Fullscreen |
85
+ | `i` | File info overlay |
86
+ | `v` | Stream with VLC |
87
+ | `+` / `-` | Zoom in / out |
88
+ | `0` | Reset zoom |
89
+ | `z` | Cycle fit mode |
90
+ | `d` | Distraction-free mode |
91
+
92
+ ### Video Action Menu
93
+
94
+ | Key | Action |
95
+ |-----|--------|
96
+ | `Space` / `Enter` | Play in browser |
97
+ | `e` | Explore video frames |
98
+ | `t` | Extract thumbnail |
99
+ | `v` | Stream with VLC |
100
+
101
+ ### Video Explorer
102
+
103
+ | Key | Action |
104
+ |-----|--------|
105
+ | `Esc` | Back to video |
106
+
107
+ ### Audio Explorer
108
+
109
+ | Key | Action |
110
+ |-----|--------|
111
+ | `Space` | Play selection |
112
+ | `+` / `-` | Zoom in / out |
113
+ | `0` | Zoom to fit |
114
+ | `z` | Zoom to selection |
115
+ | `Esc` | Clear selection / back |
116
+
117
+ ## Supported File Formats
118
+
119
+ ### Images
120
+ png, jpg, jpeg, gif, webp, svg, bmp, ico, avif, tiff, tif, heic, heif
121
+
122
+ ### Video
123
+ mp4, webm, mov, mkv, avi, wmv, flv, m4v, ogv, mpg, mpeg, mpe, mpv, m2v, ts, mts, m2ts, vob, 3gp, 3g2, f4v, rm, rmvb, divx, asf, mxf, dv, nsv, nuv, y4m, wtv, swf, dat
124
+
125
+ ### Audio
126
+ mp3, wav, ogg, flac, aac, m4a, wma, oga
127
+
128
+ ### Documents
129
+ pdf, epub, mobi
130
+
131
+ ### Archives
132
+ zip, 7z, rar, tar, tar.gz, tar.bz2, tar.xz, tgz, cbz, cbr
133
+
134
+ ### Code/Text
135
+ js, mjs, cjs, ts, tsx, jsx, py, rb, java, c, cpp, h, hpp, cs, go, rs, php, sh, bash, zsh, pl, r, swift, kt, scala, lua, json, xml, yaml, yml, toml, ini, cfg, conf, env, properties, md, markdown, txt, csv, tsv, log, html, htm, css, scss, sass, less, sql, graphql, gql, makefile, dockerfile, gitignore, editorconfig, htaccess, npmrc, eslintrc, prettierrc
136
+
137
+ ## Themes
138
+
139
+ **Grey** (pure greyscale): Daylight, Afternoon, Evening, Twilight (default), Night
140
+
141
+ **Fun** (colorful): Neo-Tokyo, Cyberpunk, Hotdog, 1970s Console, 1980s Console, 1990s Web Site, Early 2000s Web, Synthwave, Solarized Dark, Forest
142
+
143
+ ## Optional Server Tools
144
+
145
+ These external tools enhance functionality when available on the server:
146
+
147
+ | Tool | Feature |
148
+ |------|---------|
149
+ | **sharp** (npm) | Fast image thumbnail generation |
150
+ | **ImageMagick** | Fallback image thumbnails |
151
+ | **ffmpeg** | Video thumbnails, frame extraction, audio waveforms |
152
+ | **ffprobe** | Media metadata (duration, resolution, codec) |
153
+ | **7-Zip** (7z) | Archive browsing for rar, 7z, tar.* formats |
154
+ | **VLC** | External video streaming |
155
+ | **ebook-convert** (Calibre) | MOBI to EPUB conversion |
156
+
157
+ Without these tools the application still works -- images serve directly, videos play in-browser, and zip/cbz archives use native extraction.
158
+
159
+ ## API Endpoints
160
+
161
+ | Method | Endpoint | Description |
162
+ |--------|----------|-------------|
163
+ | GET | `/api/remote/settings` | Server configuration and capabilities |
164
+ | GET | `/api/filebrowser/list?path=` | Directory listing |
165
+ | PUT | `/api/filebrowser/settings` | Update browser settings (hidden files) |
166
+ | GET | `/api/media/capabilities` | Detected tool availability |
167
+ | GET | `/api/media/thumbnail?path=&width=&height=` | Generate/serve cached thumbnail |
168
+ | GET | `/api/media/probe?path=` | File metadata via ffprobe |
169
+ | GET | `/api/media/folder-summary?path=` | Media type counts for a folder |
170
+ | GET | `/api/media/video-frames?path=&count=` | Extract frames from video |
171
+ | GET | `/api/media/video-frame/:cacheKey/:filename` | Serve extracted frame |
172
+ | GET | `/api/media/video-frame-at?path=&timestamp=` | Extract frame at timestamp |
173
+ | GET | `/api/media/audio-waveform?path=&peaks=` | Audio waveform peak data |
174
+ | GET | `/api/media/audio-segment?path=&start=&end=` | Extract audio segment |
175
+ | GET | `/api/media/ebook-convert?path=` | Convert MOBI to EPUB |
176
+ | GET | `/api/media/ebook/:cacheKey/:filename` | Serve converted ebook |
177
+ | POST | `/api/media/open` | Open file in external player (VLC) |
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,14 @@
1
+ # Retold Remote <small>Documentation</small>
2
+
3
+ > A browser-based media server and NAS file explorer
4
+
5
+ Browse images, videos, audio, ebooks, code, and documents through a keyboard-driven gallery interface with 15 built-in themes. Point it at a folder and start exploring.
6
+
7
+ - **Gallery Browser** — Grid and list views with thumbnails, filtering, sorting, and text search
8
+ - **Media Viewers** — Dedicated viewers for images, video, audio, ebooks, code, and PDFs
9
+ - **Video Explorer** — Extract and browse frames from any video with a visual timeline
10
+ - **Audio Explorer** — Waveform visualization with selection, zoom, and segment playback
11
+ - **Full Keyboard Navigation** — Every feature accessible from the keyboard
12
+
13
+ [Get Started](server-setup.md)
14
+ [View on GitHub](https://github.com/stevenvelozo/retold-remote)
@@ -0,0 +1,10 @@
1
+ - [Home](/)
2
+
3
+ - Getting Started
4
+ - [Server Setup & Docker](server-setup.md)
5
+
6
+ - Media Viewers
7
+ - [Image Viewer](image-viewer.md)
8
+ - [Video Viewer & Explorer](video-viewer.md)
9
+ - [Audio Viewer & Explorer](audio-viewer.md)
10
+ - [eBook Reader](ebook-reader.md)
@@ -0,0 +1,3 @@
1
+ # Retold Remote <small>docs</small>
2
+
3
+ - [Back to App](/)