retold-remote 0.0.4 → 0.0.6
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.
- package/docs/README.md +181 -0
- package/docs/_cover.md +14 -0
- package/docs/_sidebar.md +10 -0
- package/docs/_topbar.md +3 -0
- package/docs/audio-viewer.md +133 -0
- package/docs/ebook-reader.md +90 -0
- package/docs/image-viewer.md +90 -0
- package/docs/server-setup.md +262 -0
- package/docs/video-viewer.md +134 -0
- package/html/docs.html +59 -0
- package/package.json +21 -7
- package/source/Pict-Application-RetoldRemote.js +143 -2
- package/source/RetoldRemote-ExtensionMaps.js +33 -0
- package/source/cli/RetoldRemote-Server-Setup.js +82 -67
- package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
- package/source/providers/Pict-Provider-CollectionManager.js +934 -0
- package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
- package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
- package/source/providers/Pict-Provider-GalleryNavigation.js +270 -353
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
- package/source/providers/Pict-Provider-ToastNotification.js +96 -0
- package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
- package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
- package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
- package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
- package/source/server/RetoldRemote-ArchiveService.js +2 -12
- package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
- package/source/server/RetoldRemote-CollectionService.js +684 -0
- package/source/server/RetoldRemote-EbookService.js +7 -16
- package/source/server/RetoldRemote-MediaService.js +3 -14
- package/source/server/RetoldRemote-ParimeCache.js +349 -0
- package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
- package/source/server/RetoldRemote-VideoFrameService.js +7 -15
- package/source/views/PictView-Remote-AudioExplorer.js +10 -43
- package/source/views/PictView-Remote-CollectionsPanel.js +1087 -0
- package/source/views/PictView-Remote-Gallery.js +237 -44
- package/source/views/PictView-Remote-ImageViewer.js +1 -34
- package/source/views/PictView-Remote-Layout.js +410 -20
- package/source/views/PictView-Remote-MediaViewer.js +338 -51
- package/source/views/PictView-Remote-SettingsPanel.js +155 -138
- package/source/views/PictView-Remote-TopBar.js +615 -14
- package/source/views/PictView-Remote-VLCSetup.js +766 -0
- package/source/views/PictView-Remote-VideoExplorer.js +20 -54
- package/web-application/css/docuserve.css +73 -0
- package/web-application/docs/README.md +181 -0
- package/web-application/docs/_cover.md +14 -0
- package/web-application/docs/_sidebar.md +10 -0
- package/web-application/docs/_topbar.md +3 -0
- package/web-application/docs/audio-viewer.md +133 -0
- package/web-application/docs/ebook-reader.md +90 -0
- package/web-application/docs/image-viewer.md +90 -0
- package/web-application/docs/server-setup.md +262 -0
- package/web-application/docs/video-viewer.md +134 -0
- package/web-application/docs.html +59 -0
- package/web-application/js/pict-docuserve.min.js +58 -0
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +2558 -439
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +41 -11
- package/web-application/retold-remote.min.js.map +1 -1
- package/server.js +0 -43
|
@@ -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)">← Back</button>';
|
|
403
|
-
tmpHTML += '<div class="retold-remote-vex-title">Video Explorer — ' + this.
|
|
403
|
+
tmpHTML += '<div class="retold-remote-vex-title">Video Explorer — ' + 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.
|
|
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 + '×' + 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
733
|
-
let tmpEscTimestamp = tmpSelf.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)">← Back</button>';
|
|
1002
983
|
tmpHTML += '<button class="retold-remote-vex-nav-btn" onclick="pict.views[\'RetoldRemote-VideoExplorer\'].previewPrevFrame()" title="Previous (\u2190)">‹ Prev</button>';
|
|
1003
|
-
tmpHTML += '<div class="retold-remote-vex-preview-title" id="RetoldRemote-VEX-PreviewTitle">' + this.
|
|
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 ›</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.
|
|
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.
|
|
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.
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
}
|
|
@@ -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=×tamp=` | 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,133 @@
|
|
|
1
|
+
# Audio Viewer and Explorer
|
|
2
|
+
|
|
3
|
+
The audio viewer provides HTML5 audio playback with an optional waveform explorer for visualizing, selecting, and extracting audio segments.
|
|
4
|
+
|
|
5
|
+
## Opening an Audio File
|
|
6
|
+
|
|
7
|
+
- **From the gallery**: select an audio file and press `Enter`, or double-click it
|
|
8
|
+
- **Force any file as audio**: press `3` in the gallery or viewer to open it in the audio viewer regardless of extension
|
|
9
|
+
|
|
10
|
+
## Audio Player
|
|
11
|
+
|
|
12
|
+
The viewer shows a centered audio player with the filename and native HTML5 audio controls (play/pause, seek bar, volume, playback speed).
|
|
13
|
+
|
|
14
|
+
Below the player, action buttons appear:
|
|
15
|
+
|
|
16
|
+
| Button | Requires | Description |
|
|
17
|
+
|--------|----------|-------------|
|
|
18
|
+
| **Explore Audio** | ffprobe or ffmpeg | Opens the waveform explorer |
|
|
19
|
+
| **Stream with VLC** | -- | Sends the file to VLC on the server |
|
|
20
|
+
|
|
21
|
+
### Autoplay
|
|
22
|
+
|
|
23
|
+
Autoplay is off by default. Enable it in Settings > Gallery > Autoplay audio. When enabled, audio begins playing as soon as the player loads.
|
|
24
|
+
|
|
25
|
+
## Audio Explorer
|
|
26
|
+
|
|
27
|
+
The audio explorer displays a canvas-based waveform visualization with selection, zoom, and segment extraction.
|
|
28
|
+
|
|
29
|
+
### Opening the Explorer
|
|
30
|
+
|
|
31
|
+
- From the audio player, click "Explore Audio"
|
|
32
|
+
- The explorer fetches 2000 peak samples from the server via ffmpeg
|
|
33
|
+
|
|
34
|
+
### Waveform Display
|
|
35
|
+
|
|
36
|
+
The main canvas shows the audio waveform as vertical bars. The height of each bar represents the amplitude at that point. A dashed center line marks the zero crossing.
|
|
37
|
+
|
|
38
|
+
- **Unselected regions** are drawn in the secondary text color
|
|
39
|
+
- **Selected regions** are drawn in the accent color with a semi-transparent highlight behind them
|
|
40
|
+
- A **cursor line** follows the mouse position, displayed in white
|
|
41
|
+
|
|
42
|
+
### Overview Bar
|
|
43
|
+
|
|
44
|
+
Below the main waveform, a smaller overview canvas shows the entire audio file. A highlighted viewport indicator box shows which portion of the waveform is currently visible in the main canvas.
|
|
45
|
+
|
|
46
|
+
Click anywhere on the overview to pan the main view to that position.
|
|
47
|
+
|
|
48
|
+
### Selection
|
|
49
|
+
|
|
50
|
+
**Click and drag** on the main waveform to select a time range. The selection is highlighted in the accent color with edge markers.
|
|
51
|
+
|
|
52
|
+
- If the selection is too small (less than 0.1% of the total duration), it is automatically cleared
|
|
53
|
+
- The selection is shown in both the main canvas and the overview
|
|
54
|
+
|
|
55
|
+
### Time Display Bar
|
|
56
|
+
|
|
57
|
+
A bar above the waveform shows three pieces of information:
|
|
58
|
+
|
|
59
|
+
- **View**: the time range currently visible (e.g., "0:12.3 - 0:45.7")
|
|
60
|
+
- **Selection**: the selected time range and its duration (e.g., "0:20.0 - 0:30.5 (10.5s)"), or "None"
|
|
61
|
+
- **Cursor**: the time position under the mouse, or "--"
|
|
62
|
+
|
|
63
|
+
Timestamps are formatted as `M:SS.D` (minutes, seconds, tenths of a second) or `H:MM:SS.D` for long files.
|
|
64
|
+
|
|
65
|
+
### Zoom Controls
|
|
66
|
+
|
|
67
|
+
| Key / Button | Action |
|
|
68
|
+
|--------------|--------|
|
|
69
|
+
| `+` or `=` | Zoom in (centered) |
|
|
70
|
+
| `-` | Zoom out (centered) |
|
|
71
|
+
| `0` | Zoom to fit (show entire file) |
|
|
72
|
+
| `z` | Zoom to selection (with 5% margin) |
|
|
73
|
+
| Mouse wheel | Zoom at cursor position |
|
|
74
|
+
|
|
75
|
+
Minimum zoom level is 0.5% of the total duration.
|
|
76
|
+
|
|
77
|
+
Zooming preserves the center point -- the portion of the waveform under the mouse (for wheel zoom) or the center (for keyboard zoom) stays in place while the view range narrows or widens.
|
|
78
|
+
|
|
79
|
+
### Playing a Selection
|
|
80
|
+
|
|
81
|
+
With a selection active, press `Space` or click the Play Selection button to extract and play that audio segment.
|
|
82
|
+
|
|
83
|
+
The server extracts the segment via ffmpeg and returns it as an MP3 file. A playback bar appears with an HTML5 audio player for the extracted segment.
|
|
84
|
+
|
|
85
|
+
The extraction endpoint is `/api/media/audio-segment` with `start` and `end` parameters in seconds.
|
|
86
|
+
|
|
87
|
+
### Info Bar
|
|
88
|
+
|
|
89
|
+
The explorer shows an info bar with metadata from ffprobe:
|
|
90
|
+
|
|
91
|
+
| Field | Example |
|
|
92
|
+
|-------|---------|
|
|
93
|
+
| Duration | 3:42 |
|
|
94
|
+
| Sample Rate | 44.1 kHz |
|
|
95
|
+
| Channels | 2 (stereo) |
|
|
96
|
+
| Codec | mp3 |
|
|
97
|
+
| Bitrate | 320 kbps |
|
|
98
|
+
| File Size | 8.7 MB |
|
|
99
|
+
| Peaks | 2000 (ffmpeg) |
|
|
100
|
+
|
|
101
|
+
### Keyboard Shortcuts
|
|
102
|
+
|
|
103
|
+
| Key | Action |
|
|
104
|
+
|-----|--------|
|
|
105
|
+
| `Space` | Play selection |
|
|
106
|
+
| `+` / `=` | Zoom in |
|
|
107
|
+
| `-` | Zoom out |
|
|
108
|
+
| `0` | Zoom to fit |
|
|
109
|
+
| `z` | Zoom to selection |
|
|
110
|
+
| `Esc` | Clear selection (if any), or go back to audio player |
|
|
111
|
+
|
|
112
|
+
### Mouse Interactions
|
|
113
|
+
|
|
114
|
+
| Action | Effect |
|
|
115
|
+
|--------|--------|
|
|
116
|
+
| Click and drag on main waveform | Create/adjust selection |
|
|
117
|
+
| Mouse move on main waveform | Show cursor position in time bar |
|
|
118
|
+
| Mouse wheel on main waveform | Zoom in/out at cursor position |
|
|
119
|
+
| Click on overview bar | Pan main view to that position |
|
|
120
|
+
|
|
121
|
+
## File Info Overlay
|
|
122
|
+
|
|
123
|
+
Press `i` while viewing audio to see metadata:
|
|
124
|
+
|
|
125
|
+
- Size, Duration, Codec, Bitrate, Format, Modified date, Path
|
|
126
|
+
|
|
127
|
+
## Supported Formats
|
|
128
|
+
|
|
129
|
+
mp3, wav, ogg, flac, aac, m4a, wma, oga
|
|
130
|
+
|
|
131
|
+
Browser playback support varies. Most browsers play mp3, wav, ogg, and flac natively. For other formats, use VLC streaming.
|
|
132
|
+
|
|
133
|
+
Force any file as audio by pressing `3` in the gallery or viewer.
|