retold-remote 0.0.1 → 0.0.2

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 (33) hide show
  1. package/html/index.html +2 -0
  2. package/package.json +20 -14
  3. package/source/Pict-Application-RetoldRemote.js +46 -5
  4. package/source/cli/RetoldRemote-CLI-Run.js +0 -0
  5. package/source/cli/RetoldRemote-Server-Setup.js +790 -8
  6. package/source/cli/commands/RetoldRemote-Command-Serve.js +34 -1
  7. package/source/providers/Pict-Provider-GalleryFilterSort.js +61 -9
  8. package/source/providers/Pict-Provider-GalleryNavigation.js +517 -18
  9. package/source/providers/Pict-Provider-RetoldRemote.js +11 -2
  10. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +1 -0
  11. package/source/server/RetoldRemote-ArchiveService.js +830 -0
  12. package/source/server/RetoldRemote-AudioWaveformService.js +673 -0
  13. package/source/server/RetoldRemote-EbookService.js +242 -0
  14. package/source/server/RetoldRemote-MediaService.js +1 -1
  15. package/source/server/RetoldRemote-ToolDetector.js +31 -1
  16. package/source/server/RetoldRemote-VideoFrameService.js +486 -0
  17. package/source/views/PictView-Remote-AudioExplorer.js +1213 -0
  18. package/source/views/PictView-Remote-Gallery.js +141 -2
  19. package/source/views/PictView-Remote-Layout.js +18 -27
  20. package/source/views/PictView-Remote-MediaViewer.js +638 -39
  21. package/source/views/PictView-Remote-SettingsPanel.js +23 -0
  22. package/source/views/PictView-Remote-TopBar.js +121 -0
  23. package/source/views/PictView-Remote-VideoExplorer.js +1229 -0
  24. package/web-application/index.html +2 -0
  25. package/web-application/js/epub.min.js +1 -0
  26. package/web-application/retold-remote.js +7030 -1244
  27. package/web-application/retold-remote.js.map +1 -1
  28. package/web-application/retold-remote.min.js +13 -44
  29. package/web-application/retold-remote.min.js.map +1 -1
  30. package/web-application/retold-remote.compatible.js +0 -5764
  31. package/web-application/retold-remote.compatible.js.map +0 -1
  32. package/web-application/retold-remote.compatible.min.js +0 -120
  33. package/web-application/retold-remote.compatible.min.js.map +0 -1
@@ -147,6 +147,213 @@ const _ViewConfiguration =
147
147
  .retold-remote-code-viewer-container .pict-code-editor .tag { color: #E06C75; }
148
148
  .retold-remote-code-viewer-container .pict-code-editor .attr-name { color: #D19A66; }
149
149
  .retold-remote-code-viewer-container .pict-code-editor .attr-value { color: #98C379; }
150
+ /* Video wrap with stats bar */
151
+ .retold-remote-video-wrap
152
+ {
153
+ display: flex;
154
+ flex-direction: column;
155
+ align-items: center;
156
+ max-width: 100%;
157
+ max-height: 100%;
158
+ width: 100%;
159
+ height: 100%;
160
+ }
161
+ .retold-remote-video-wrap video
162
+ {
163
+ flex: 1;
164
+ min-height: 0;
165
+ max-width: 100%;
166
+ max-height: calc(100% - 40px);
167
+ object-fit: contain;
168
+ }
169
+ .retold-remote-video-stats
170
+ {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 16px;
174
+ padding: 6px 16px;
175
+ background: var(--retold-bg-secondary);
176
+ border-top: 1px solid var(--retold-border);
177
+ width: 100%;
178
+ flex-shrink: 0;
179
+ font-size: 0.75rem;
180
+ color: var(--retold-text-dim);
181
+ white-space: nowrap;
182
+ overflow-x: auto;
183
+ }
184
+ .retold-remote-video-stats span
185
+ {
186
+ display: inline-flex;
187
+ align-items: center;
188
+ gap: 4px;
189
+ }
190
+ .retold-remote-video-stats .retold-remote-video-stat-label
191
+ {
192
+ color: var(--retold-text-muted);
193
+ }
194
+ .retold-remote-video-stats .retold-remote-video-stat-value
195
+ {
196
+ color: var(--retold-text-secondary);
197
+ }
198
+ .retold-remote-explore-btn
199
+ {
200
+ margin-left: auto;
201
+ padding: 3px 12px;
202
+ border: 1px solid var(--retold-accent);
203
+ border-radius: 3px;
204
+ background: transparent;
205
+ color: var(--retold-accent);
206
+ font-size: 0.75rem;
207
+ cursor: pointer;
208
+ transition: background 0.15s, color 0.15s;
209
+ font-family: inherit;
210
+ white-space: nowrap;
211
+ }
212
+ .retold-remote-explore-btn:hover
213
+ {
214
+ background: var(--retold-accent);
215
+ color: var(--retold-bg-primary);
216
+ }
217
+ .retold-remote-vlc-btn
218
+ {
219
+ padding: 3px 12px;
220
+ border: 1px solid var(--retold-accent);
221
+ border-radius: 3px;
222
+ background: transparent;
223
+ color: var(--retold-accent);
224
+ font-size: 0.75rem;
225
+ cursor: pointer;
226
+ transition: background 0.15s, color 0.15s;
227
+ font-family: inherit;
228
+ white-space: nowrap;
229
+ }
230
+ .retold-remote-vlc-btn:hover
231
+ {
232
+ background: var(--retold-accent);
233
+ color: var(--retold-bg-primary);
234
+ }
235
+ /* Ebook reader */
236
+ .retold-remote-ebook-wrap
237
+ {
238
+ display: flex;
239
+ width: 100%;
240
+ height: 100%;
241
+ position: relative;
242
+ }
243
+ .retold-remote-ebook-toc
244
+ {
245
+ width: 240px;
246
+ flex-shrink: 0;
247
+ background: var(--retold-bg-secondary);
248
+ border-right: 1px solid var(--retold-border);
249
+ overflow-y: auto;
250
+ font-size: 0.78rem;
251
+ padding: 8px 0;
252
+ }
253
+ .retold-remote-ebook-toc.collapsed
254
+ {
255
+ display: none;
256
+ }
257
+ .retold-remote-ebook-toc-item
258
+ {
259
+ display: block;
260
+ padding: 6px 16px;
261
+ color: var(--retold-text-secondary);
262
+ text-decoration: none;
263
+ cursor: pointer;
264
+ transition: background 0.1s, color 0.1s;
265
+ border: none;
266
+ background: none;
267
+ width: 100%;
268
+ text-align: left;
269
+ font-family: inherit;
270
+ font-size: inherit;
271
+ }
272
+ .retold-remote-ebook-toc-item:hover
273
+ {
274
+ background: var(--retold-bg-tertiary);
275
+ color: var(--retold-text-primary);
276
+ }
277
+ .retold-remote-ebook-toc-item.indent-1
278
+ {
279
+ padding-left: 32px;
280
+ }
281
+ .retold-remote-ebook-toc-item.indent-2
282
+ {
283
+ padding-left: 48px;
284
+ }
285
+ .retold-remote-ebook-reader
286
+ {
287
+ flex: 1;
288
+ display: flex;
289
+ flex-direction: column;
290
+ min-width: 0;
291
+ position: relative;
292
+ }
293
+ .retold-remote-ebook-content
294
+ {
295
+ flex: 1;
296
+ position: relative;
297
+ overflow: hidden;
298
+ }
299
+ .retold-remote-ebook-content iframe
300
+ {
301
+ border: none;
302
+ }
303
+ .retold-remote-ebook-controls
304
+ {
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: center;
308
+ gap: 16px;
309
+ padding: 8px 16px;
310
+ background: var(--retold-bg-secondary);
311
+ border-top: 1px solid var(--retold-border);
312
+ flex-shrink: 0;
313
+ }
314
+ .retold-remote-ebook-page-btn
315
+ {
316
+ padding: 6px 20px;
317
+ border: 1px solid var(--retold-border);
318
+ border-radius: 3px;
319
+ background: transparent;
320
+ color: var(--retold-text-muted);
321
+ font-size: 0.82rem;
322
+ cursor: pointer;
323
+ transition: color 0.15s, border-color 0.15s;
324
+ font-family: inherit;
325
+ }
326
+ .retold-remote-ebook-page-btn:hover
327
+ {
328
+ color: var(--retold-text-primary);
329
+ border-color: var(--retold-accent);
330
+ }
331
+ .retold-remote-ebook-toc-btn
332
+ {
333
+ padding: 6px 12px;
334
+ border: 1px solid var(--retold-border);
335
+ border-radius: 3px;
336
+ background: transparent;
337
+ color: var(--retold-text-muted);
338
+ font-size: 0.75rem;
339
+ cursor: pointer;
340
+ font-family: inherit;
341
+ }
342
+ .retold-remote-ebook-toc-btn:hover
343
+ {
344
+ color: var(--retold-text-primary);
345
+ border-color: var(--retold-accent);
346
+ }
347
+ .retold-remote-ebook-loading
348
+ {
349
+ display: flex;
350
+ flex-direction: column;
351
+ align-items: center;
352
+ justify-content: center;
353
+ height: 100%;
354
+ color: var(--retold-text-dim);
355
+ font-size: 0.85rem;
356
+ }
150
357
  `
151
358
  };
152
359
 
@@ -241,6 +448,16 @@ class RetoldRemoteMediaViewerView extends libPictView
241
448
  this._loadCodeViewer(tmpContentURL, pFilePath);
242
449
  }
243
450
 
451
+ // Load ebook viewer for epub/mobi
452
+ if (pMediaType === 'document')
453
+ {
454
+ let tmpExt = pFilePath.replace(/^.*\./, '').toLowerCase();
455
+ if (tmpExt === 'epub' || tmpExt === 'mobi')
456
+ {
457
+ this._loadEbookViewer(tmpContentURL, pFilePath);
458
+ }
459
+ }
460
+
244
461
  // Update topbar
245
462
  let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
246
463
  if (tmpTopBar)
@@ -260,26 +477,74 @@ class RetoldRemoteMediaViewerView extends libPictView
260
477
 
261
478
  _buildVideoHTML(pURL, pFileName)
262
479
  {
263
- return '<video controls autoplay preload="metadata" '
264
- + 'style="max-width: 100%; max-height: 100%;" '
480
+ let tmpHTML = '<div class="retold-remote-video-wrap">';
481
+
482
+ let tmpAutoplayVideo = this.pict.AppData.RetoldRemote.AutoplayVideo ? ' autoplay' : '';
483
+ tmpHTML += '<video controls' + tmpAutoplayVideo + ' preload="metadata" '
265
484
  + 'id="RetoldRemote-VideoPlayer">'
266
485
  + '<source src="' + pURL + '">'
267
486
  + 'Your browser does not support the video tag.'
268
487
  + '</video>';
488
+
489
+ // Stats bar below the video
490
+ tmpHTML += '<div class="retold-remote-video-stats" id="RetoldRemote-VideoStats">';
491
+ tmpHTML += '<span class="retold-remote-video-stat-label">Loading info...</span>';
492
+
493
+ // Explore Video button (only when ffmpeg is available)
494
+ let tmpCapabilities = this.pict.AppData.RetoldRemote.ServerCapabilities || {};
495
+ if (tmpCapabilities.ffmpeg)
496
+ {
497
+ tmpHTML += '<button class="retold-remote-explore-btn" '
498
+ + 'onclick="pict.views[\'RetoldRemote-VideoExplorer\'].showExplorer(pict.AppData.RetoldRemote.CurrentViewerFile)" '
499
+ + 'title="Explore frames from this video">'
500
+ + '&#128270; Explore Video'
501
+ + '</button>';
502
+ }
503
+
504
+ // VLC button (only shown when VLC capability is available)
505
+ if (tmpCapabilities.vlc)
506
+ {
507
+ tmpHTML += '<button class="retold-remote-vlc-btn" '
508
+ + 'onclick="pict.providers[\'RetoldRemote-GalleryNavigation\']._openWithVLC()" '
509
+ + 'title="Open with VLC (Enter)">'
510
+ + '&#9654; Open ' + this._escapeHTML(pFileName) + ' with VLC'
511
+ + '</button>';
512
+ }
513
+
514
+ tmpHTML += '</div>'; // end stats
515
+ tmpHTML += '</div>'; // end wrap
516
+
517
+ return tmpHTML;
269
518
  }
270
519
 
271
520
  _buildAudioHTML(pURL, pFileName)
272
521
  {
273
522
  let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
274
523
  let tmpIconHTML = tmpIconProvider ? '<span class="retold-remote-icon retold-remote-icon-lg">' + tmpIconProvider.getIcon('music-note', 64) + '</span>' : '&#127925;';
275
- return '<div style="text-align: center; padding: 40px;">'
524
+
525
+ let tmpHTML = '<div style="text-align: center; padding: 40px;">'
276
526
  + '<div style="margin-bottom: 24px;">' + tmpIconHTML + '</div>'
277
527
  + '<div style="font-size: 1.1rem; color: var(--retold-text-secondary); margin-bottom: 24px;">' + this._escapeHTML(pFileName) + '</div>'
278
- + '<audio controls autoplay preload="metadata" id="RetoldRemote-AudioPlayer" style="width: 100%; max-width: 500px;">'
528
+ + '<audio controls' + (this.pict.AppData.RetoldRemote.AutoplayAudio ? ' autoplay' : '') + ' preload="metadata" id="RetoldRemote-AudioPlayer" style="width: 100%; max-width: 500px;">'
279
529
  + '<source src="' + pURL + '">'
280
530
  + 'Your browser does not support the audio tag.'
281
- + '</audio>'
282
- + '</div>';
531
+ + '</audio>';
532
+
533
+ // Explore Audio button (available when ffprobe is present)
534
+ let tmpCapabilities = this.pict.AppData.RetoldRemote.ServerCapabilities || {};
535
+ if (tmpCapabilities.ffprobe || tmpCapabilities.ffmpeg)
536
+ {
537
+ tmpHTML += '<div style="margin-top: 20px;">'
538
+ + '<button class="retold-remote-explore-btn" '
539
+ + 'onclick="pict.views[\'RetoldRemote-AudioExplorer\'].showExplorer(pict.AppData.RetoldRemote.CurrentViewerFile)" '
540
+ + 'title="Explore waveform and extract segments from this audio">'
541
+ + '&#128202; Explore Audio'
542
+ + '</button>'
543
+ + '</div>';
544
+ }
545
+
546
+ tmpHTML += '</div>';
547
+ return tmpHTML;
283
548
  }
284
549
 
285
550
  _buildDocumentHTML(pURL, pFileName, pFilePath)
@@ -293,6 +558,11 @@ class RetoldRemoteMediaViewerView extends libPictView
293
558
  + '</iframe>';
294
559
  }
295
560
 
561
+ if (tmpExtension === 'epub' || tmpExtension === 'mobi')
562
+ {
563
+ return this._buildEbookHTML(pURL, pFileName, pFilePath);
564
+ }
565
+
296
566
  // For other document types, show a download link
297
567
  let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
298
568
  let tmpDocIconHTML = tmpIconProvider ? '<span class="retold-remote-icon retold-remote-icon-lg">' + tmpIconProvider.getIcon('document-large', 64) + '</span>' : '&#128196;';
@@ -440,6 +710,278 @@ class RetoldRemoteMediaViewerView extends libPictView
440
710
  });
441
711
  }
442
712
 
713
+ /**
714
+ * Build the HTML shell for the ebook reader.
715
+ */
716
+ _buildEbookHTML(pURL, pFileName, pFilePath)
717
+ {
718
+ return '<div class="retold-remote-ebook-wrap">'
719
+ + '<div class="retold-remote-ebook-toc collapsed" id="RetoldRemote-EbookTOC"></div>'
720
+ + '<div class="retold-remote-ebook-reader">'
721
+ + '<div class="retold-remote-ebook-content" id="RetoldRemote-EbookContent">'
722
+ + '<div class="retold-remote-ebook-loading">Loading ebook...</div>'
723
+ + '</div>'
724
+ + '<div class="retold-remote-ebook-controls">'
725
+ + '<button class="retold-remote-ebook-toc-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].toggleEbookTOC()">&#9776; TOC</button>'
726
+ + '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookPrevPage()">&larr; Prev</button>'
727
+ + '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookNextPage()">Next &rarr;</button>'
728
+ + '</div>'
729
+ + '</div>'
730
+ + '</div>';
731
+ }
732
+
733
+ /**
734
+ * Load and render an ebook using epub.js.
735
+ * For EPUB files, fetch directly. For MOBI files, convert server-side first.
736
+ *
737
+ * @param {string} pContentURL - Content URL for the file
738
+ * @param {string} pFilePath - Relative file path
739
+ */
740
+ _loadEbookViewer(pContentURL, pFilePath)
741
+ {
742
+ let tmpSelf = this;
743
+ let tmpExtension = pFilePath.replace(/^.*\./, '').toLowerCase();
744
+
745
+ if (tmpExtension === 'mobi')
746
+ {
747
+ // Convert MOBI to EPUB server-side first
748
+ let tmpCapabilities = this.pict.AppData.RetoldRemote.ServerCapabilities || {};
749
+ if (!tmpCapabilities.ebook_convert)
750
+ {
751
+ let tmpContent = document.getElementById('RetoldRemote-EbookContent');
752
+ if (tmpContent)
753
+ {
754
+ tmpContent.innerHTML = '<div class="retold-remote-ebook-loading">'
755
+ + 'MOBI viewing requires Calibre (ebook-convert) on the server.<br>'
756
+ + '<a href="' + pContentURL + '" target="_blank" style="color: var(--retold-accent); margin-top: 12px; display: inline-block;">Download file</a>'
757
+ + '</div>';
758
+ }
759
+ return;
760
+ }
761
+
762
+ let tmpContent = document.getElementById('RetoldRemote-EbookContent');
763
+ if (tmpContent)
764
+ {
765
+ tmpContent.innerHTML = '<div class="retold-remote-ebook-loading">Converting MOBI to EPUB...</div>';
766
+ }
767
+
768
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
769
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
770
+
771
+ fetch('/api/media/ebook-convert?path=' + tmpPathParam)
772
+ .then((pResponse) => pResponse.json())
773
+ .then((pData) =>
774
+ {
775
+ if (!pData || !pData.Success)
776
+ {
777
+ throw new Error(pData ? pData.Error : 'Conversion failed.');
778
+ }
779
+
780
+ // Fetch the converted EPUB and render
781
+ let tmpEpubURL = '/api/media/ebook/' + pData.CacheKey + '/' + pData.OutputFilename;
782
+ tmpSelf._renderEpub(tmpEpubURL);
783
+ })
784
+ .catch((pError) =>
785
+ {
786
+ let tmpEl = document.getElementById('RetoldRemote-EbookContent');
787
+ if (tmpEl)
788
+ {
789
+ tmpEl.innerHTML = '<div class="retold-remote-ebook-loading">Failed to convert: '
790
+ + tmpSelf._escapeHTML(pError.message)
791
+ + '<br><a href="' + pContentURL + '" target="_blank" style="color: var(--retold-accent); margin-top: 12px; display: inline-block;">Download file</a>'
792
+ + '</div>';
793
+ }
794
+ });
795
+ }
796
+ else
797
+ {
798
+ // EPUB — render directly
799
+ this._renderEpub(pContentURL);
800
+ }
801
+ }
802
+
803
+ /**
804
+ * Initialize epub.js and render an EPUB into the viewer container.
805
+ *
806
+ * @param {string} pEpubURL - URL to fetch the EPUB from
807
+ */
808
+ _renderEpub(pEpubURL)
809
+ {
810
+ let tmpSelf = this;
811
+
812
+ // Check that epub.js is available
813
+ if (typeof (window) === 'undefined' || typeof (window.ePub) !== 'function')
814
+ {
815
+ let tmpEl = document.getElementById('RetoldRemote-EbookContent');
816
+ if (tmpEl)
817
+ {
818
+ tmpEl.innerHTML = '<div class="retold-remote-ebook-loading">epub.js library not loaded.</div>';
819
+ }
820
+ return;
821
+ }
822
+
823
+ // Destroy any previous book instance
824
+ if (this._activeBook)
825
+ {
826
+ try { this._activeBook.destroy(); } catch (e) { /* ignore */ }
827
+ this._activeBook = null;
828
+ this._activeRendition = null;
829
+ }
830
+
831
+ let tmpContentEl = document.getElementById('RetoldRemote-EbookContent');
832
+ if (!tmpContentEl)
833
+ {
834
+ return;
835
+ }
836
+
837
+ // Clear loading message
838
+ tmpContentEl.innerHTML = '';
839
+
840
+ // Fetch the EPUB as an ArrayBuffer and open with epub.js
841
+ fetch(pEpubURL)
842
+ .then((pResponse) =>
843
+ {
844
+ if (!pResponse.ok)
845
+ {
846
+ throw new Error('HTTP ' + pResponse.status);
847
+ }
848
+ return pResponse.arrayBuffer();
849
+ })
850
+ .then((pBuffer) =>
851
+ {
852
+ let tmpBook = window.ePub(pBuffer);
853
+ tmpSelf._activeBook = tmpBook;
854
+
855
+ let tmpRendition = tmpBook.renderTo(tmpContentEl,
856
+ {
857
+ width: '100%',
858
+ height: '100%',
859
+ spread: 'none'
860
+ });
861
+
862
+ tmpSelf._activeRendition = tmpRendition;
863
+
864
+ tmpRendition.display();
865
+
866
+ // Apply theme for dark backgrounds
867
+ tmpRendition.themes.default(
868
+ {
869
+ 'body':
870
+ {
871
+ 'color': 'var(--retold-text-primary, #d4d4d4)',
872
+ 'background': 'var(--retold-bg-primary, #1e1e1e)',
873
+ 'font-family': 'Georgia, "Times New Roman", serif',
874
+ 'line-height': '1.6',
875
+ 'padding': '20px 40px'
876
+ },
877
+ 'a':
878
+ {
879
+ 'color': 'var(--retold-accent, #569cd6)'
880
+ }
881
+ });
882
+
883
+ // Load table of contents
884
+ tmpBook.loaded.navigation.then((pNav) =>
885
+ {
886
+ tmpSelf._renderEbookTOC(pNav.toc);
887
+ });
888
+ })
889
+ .catch((pError) =>
890
+ {
891
+ if (tmpContentEl)
892
+ {
893
+ tmpContentEl.innerHTML = '<div class="retold-remote-ebook-loading">Failed to load ebook: '
894
+ + tmpSelf._escapeHTML(pError.message) + '</div>';
895
+ }
896
+ });
897
+ }
898
+
899
+ /**
900
+ * Render the table of contents for the ebook.
901
+ *
902
+ * @param {Array} pToc - epub.js navigation TOC array
903
+ */
904
+ _renderEbookTOC(pToc)
905
+ {
906
+ let tmpTocEl = document.getElementById('RetoldRemote-EbookTOC');
907
+ if (!tmpTocEl || !pToc)
908
+ {
909
+ return;
910
+ }
911
+
912
+ let tmpSelf = this;
913
+ let tmpHTML = '';
914
+
915
+ let tmpBuildItems = function (pItems, pDepth)
916
+ {
917
+ for (let i = 0; i < pItems.length; i++)
918
+ {
919
+ let tmpItem = pItems[i];
920
+ let tmpIndentClass = pDepth > 0 ? ' indent-' + Math.min(pDepth, 2) : '';
921
+ tmpHTML += '<button class="retold-remote-ebook-toc-item' + tmpIndentClass + '" '
922
+ + 'data-href="' + tmpSelf._escapeHTML(tmpItem.href) + '" '
923
+ + 'onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookGoToChapter(this.getAttribute(\'data-href\'))">'
924
+ + tmpSelf._escapeHTML(tmpItem.label.trim())
925
+ + '</button>';
926
+
927
+ if (tmpItem.subitems && tmpItem.subitems.length > 0)
928
+ {
929
+ tmpBuildItems(tmpItem.subitems, pDepth + 1);
930
+ }
931
+ }
932
+ };
933
+
934
+ tmpBuildItems(pToc, 0);
935
+ tmpTocEl.innerHTML = tmpHTML;
936
+ }
937
+
938
+ /**
939
+ * Navigate to a chapter in the ebook by href.
940
+ *
941
+ * @param {string} pHref - Chapter href from the TOC
942
+ */
943
+ ebookGoToChapter(pHref)
944
+ {
945
+ if (this._activeRendition && pHref)
946
+ {
947
+ this._activeRendition.display(pHref);
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Go to the previous page in the ebook.
953
+ */
954
+ ebookPrevPage()
955
+ {
956
+ if (this._activeRendition)
957
+ {
958
+ this._activeRendition.prev();
959
+ }
960
+ }
961
+
962
+ /**
963
+ * Go to the next page in the ebook.
964
+ */
965
+ ebookNextPage()
966
+ {
967
+ if (this._activeRendition)
968
+ {
969
+ this._activeRendition.next();
970
+ }
971
+ }
972
+
973
+ /**
974
+ * Toggle the table of contents sidebar.
975
+ */
976
+ toggleEbookTOC()
977
+ {
978
+ let tmpTocEl = document.getElementById('RetoldRemote-EbookTOC');
979
+ if (tmpTocEl)
980
+ {
981
+ tmpTocEl.classList.toggle('collapsed');
982
+ }
983
+ }
984
+
443
985
  _buildFallbackHTML(pURL, pFileName)
444
986
  {
445
987
  let tmpIconProvider = this.pict.providers['RetoldRemote-Icons'];
@@ -452,10 +994,11 @@ class RetoldRemoteMediaViewerView extends libPictView
452
994
  }
453
995
 
454
996
  /**
455
- * Fetch file info and populate the overlay.
997
+ * Fetch file info and populate the overlay and video stats bar.
456
998
  */
457
999
  _loadFileInfo(pFilePath)
458
1000
  {
1001
+ let tmpSelf = this;
459
1002
  let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
460
1003
  if (!tmpProvider)
461
1004
  {
@@ -465,46 +1008,102 @@ class RetoldRemoteMediaViewerView extends libPictView
465
1008
  tmpProvider.fetchMediaProbe(pFilePath,
466
1009
  (pError, pData) =>
467
1010
  {
468
- let tmpOverlay = document.getElementById('RetoldRemote-FileInfo-Overlay');
469
- if (!tmpOverlay || !pData)
1011
+ if (!pData)
470
1012
  {
471
1013
  return;
472
1014
  }
473
1015
 
474
- let tmpHTML = '';
475
-
476
- if (pData.Size !== undefined)
477
- {
478
- tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Size</span><span class="retold-remote-fileinfo-value">' + this._formatFileSize(pData.Size) + '</span></div>';
479
- }
480
- if (pData.Width && pData.Height)
481
- {
482
- tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Dimensions</span><span class="retold-remote-fileinfo-value">' + pData.Width + ' x ' + pData.Height + '</span></div>';
483
- }
484
- if (pData.Duration)
485
- {
486
- let tmpMin = Math.floor(pData.Duration / 60);
487
- let tmpSec = Math.floor(pData.Duration % 60);
488
- tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Duration</span><span class="retold-remote-fileinfo-value">' + tmpMin + ':' + (tmpSec < 10 ? '0' : '') + tmpSec + '</span></div>';
489
- }
490
- if (pData.Codec)
491
- {
492
- tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Codec</span><span class="retold-remote-fileinfo-value">' + pData.Codec + '</span></div>';
493
- }
494
- if (pData.Format)
495
- {
496
- tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Format</span><span class="retold-remote-fileinfo-value">' + pData.Format + '</span></div>';
497
- }
498
- if (pData.Modified)
1016
+ // Populate the info overlay
1017
+ let tmpOverlay = document.getElementById('RetoldRemote-FileInfo-Overlay');
1018
+ if (tmpOverlay)
499
1019
  {
500
- tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Modified</span><span class="retold-remote-fileinfo-value">' + new Date(pData.Modified).toLocaleString() + '</span></div>';
1020
+ let tmpHTML = '';
1021
+
1022
+ if (pData.Size !== undefined)
1023
+ {
1024
+ tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Size</span><span class="retold-remote-fileinfo-value">' + tmpSelf._formatFileSize(pData.Size) + '</span></div>';
1025
+ }
1026
+ if (pData.Width && pData.Height)
1027
+ {
1028
+ tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Dimensions</span><span class="retold-remote-fileinfo-value">' + pData.Width + ' x ' + pData.Height + '</span></div>';
1029
+ }
1030
+ if (pData.Duration)
1031
+ {
1032
+ let tmpMin = Math.floor(pData.Duration / 60);
1033
+ let tmpSec = Math.floor(pData.Duration % 60);
1034
+ tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Duration</span><span class="retold-remote-fileinfo-value">' + tmpMin + ':' + (tmpSec < 10 ? '0' : '') + tmpSec + '</span></div>';
1035
+ }
1036
+ if (pData.Codec)
1037
+ {
1038
+ tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Codec</span><span class="retold-remote-fileinfo-value">' + pData.Codec + '</span></div>';
1039
+ }
1040
+ if (pData.Format)
1041
+ {
1042
+ tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Format</span><span class="retold-remote-fileinfo-value">' + pData.Format + '</span></div>';
1043
+ }
1044
+ if (pData.Modified)
1045
+ {
1046
+ tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Modified</span><span class="retold-remote-fileinfo-value">' + new Date(pData.Modified).toLocaleString() + '</span></div>';
1047
+ }
1048
+ if (pData.Path)
1049
+ {
1050
+ tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Path</span><span class="retold-remote-fileinfo-value">' + pData.Path + '</span></div>';
1051
+ }
1052
+
1053
+ tmpOverlay.innerHTML = tmpHTML;
501
1054
  }
502
- if (pData.Path)
1055
+
1056
+ // Populate the video stats bar (if viewing a video)
1057
+ let tmpStatsBar = document.getElementById('RetoldRemote-VideoStats');
1058
+ if (tmpStatsBar)
503
1059
  {
504
- tmpHTML += '<div class="retold-remote-fileinfo-row"><span class="retold-remote-fileinfo-label">Path</span><span class="retold-remote-fileinfo-value">' + pData.Path + '</span></div>';
505
- }
1060
+ let tmpStatsHTML = '';
1061
+
1062
+ if (pData.Duration)
1063
+ {
1064
+ let tmpMin = Math.floor(pData.Duration / 60);
1065
+ let tmpSec = Math.floor(pData.Duration % 60);
1066
+ tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Duration</span> <span class="retold-remote-video-stat-value">' + tmpMin + ':' + (tmpSec < 10 ? '0' : '') + tmpSec + '</span></span>';
1067
+ }
1068
+ if (pData.Width && pData.Height)
1069
+ {
1070
+ tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Resolution</span> <span class="retold-remote-video-stat-value">' + pData.Width + '×' + pData.Height + '</span></span>';
1071
+ }
1072
+ if (pData.Codec)
1073
+ {
1074
+ tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Codec</span> <span class="retold-remote-video-stat-value">' + pData.Codec + '</span></span>';
1075
+ }
1076
+ if (pData.Bitrate)
1077
+ {
1078
+ let tmpBitrate = pData.Bitrate;
1079
+ let tmpBitrateStr;
1080
+ if (tmpBitrate >= 1000000)
1081
+ {
1082
+ tmpBitrateStr = (tmpBitrate / 1000000).toFixed(1) + ' Mbps';
1083
+ }
1084
+ else if (tmpBitrate >= 1000)
1085
+ {
1086
+ tmpBitrateStr = Math.round(tmpBitrate / 1000) + ' kbps';
1087
+ }
1088
+ else
1089
+ {
1090
+ tmpBitrateStr = tmpBitrate + ' bps';
1091
+ }
1092
+ tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Bitrate</span> <span class="retold-remote-video-stat-value">' + tmpBitrateStr + '</span></span>';
1093
+ }
1094
+ if (pData.Size !== undefined)
1095
+ {
1096
+ tmpStatsHTML += '<span><span class="retold-remote-video-stat-label">Size</span> <span class="retold-remote-video-stat-value">' + tmpSelf._formatFileSize(pData.Size) + '</span></span>';
1097
+ }
506
1098
 
507
- tmpOverlay.innerHTML = tmpHTML;
1099
+ // Preserve the Explore and VLC buttons if they exist
1100
+ let tmpExploreBtn = tmpStatsBar.querySelector('.retold-remote-explore-btn');
1101
+ let tmpExploreHTML = tmpExploreBtn ? tmpExploreBtn.outerHTML : '';
1102
+ let tmpVLCBtn = tmpStatsBar.querySelector('.retold-remote-vlc-btn');
1103
+ let tmpVLCHTML = tmpVLCBtn ? tmpVLCBtn.outerHTML : '';
1104
+
1105
+ tmpStatsBar.innerHTML = tmpStatsHTML + tmpExploreHTML + tmpVLCHTML;
1106
+ }
508
1107
  });
509
1108
  }
510
1109