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
@@ -16,6 +16,8 @@ class GalleryNavigationProvider extends libPictProvider
16
16
  this._columnsPerRow = 4;
17
17
  this._keydownBound = false;
18
18
  this._helpPanelVisible = false;
19
+ this._sidebarFocused = false;
20
+ this._sidebarCursorIndex = 0;
19
21
  }
20
22
 
21
23
  /**
@@ -79,6 +81,55 @@ class GalleryNavigationProvider extends libPictProvider
79
81
  return;
80
82
  }
81
83
 
84
+ // F9 toggles sidebar focus from any mode
85
+ if (pEvent.key === 'F9')
86
+ {
87
+ pEvent.preventDefault();
88
+ if (tmpSelf._sidebarFocused)
89
+ {
90
+ tmpSelf._blurSidebar();
91
+ }
92
+ else
93
+ {
94
+ tmpSelf._focusSidebar();
95
+ }
96
+ return;
97
+ }
98
+
99
+ // / toggles filter bar from any context (including when search is focused)
100
+ if (pEvent.key === '/')
101
+ {
102
+ // If the search input is currently focused, / should hide the bar
103
+ let tmpSearchInput = document.getElementById('RetoldRemote-Gallery-Search');
104
+ if (pEvent.target === tmpSearchInput)
105
+ {
106
+ pEvent.preventDefault();
107
+ tmpSelf._hideFilterBar();
108
+ tmpSearchInput.blur();
109
+ return;
110
+ }
111
+
112
+ // If another input is focused, let it type normally
113
+ if (pEvent.target.tagName === 'INPUT' || pEvent.target.tagName === 'TEXTAREA' || pEvent.target.isContentEditable)
114
+ {
115
+ return;
116
+ }
117
+
118
+ // Otherwise toggle the filter bar
119
+ pEvent.preventDefault();
120
+ tmpSelf._toggleFilterBar();
121
+ return;
122
+ }
123
+
124
+ // Escape from the search input hides the filter bar
125
+ if (pEvent.key === 'Escape' && pEvent.target.id === 'RetoldRemote-Gallery-Search')
126
+ {
127
+ pEvent.preventDefault();
128
+ pEvent.target.blur();
129
+ tmpSelf._hideFilterBar();
130
+ return;
131
+ }
132
+
82
133
  // Don't capture keys when an input is focused
83
134
  if (pEvent.target.tagName === 'INPUT' || pEvent.target.tagName === 'TEXTAREA' || pEvent.target.isContentEditable)
84
135
  {
@@ -96,10 +147,22 @@ class GalleryNavigationProvider extends libPictProvider
96
147
  let tmpRemote = tmpSelf.pict.AppData.RetoldRemote;
97
148
  let tmpActiveMode = tmpRemote.ActiveMode;
98
149
 
99
- if (tmpActiveMode === 'gallery')
150
+ if (tmpActiveMode === 'gallery' && tmpSelf._sidebarFocused)
151
+ {
152
+ tmpSelf._handleSidebarKey(pEvent);
153
+ }
154
+ else if (tmpActiveMode === 'gallery')
100
155
  {
101
156
  tmpSelf._handleGalleryKey(pEvent);
102
157
  }
158
+ else if (tmpActiveMode === 'video-explorer')
159
+ {
160
+ tmpSelf._handleVideoExplorerKey(pEvent);
161
+ }
162
+ else if (tmpActiveMode === 'audio-explorer')
163
+ {
164
+ tmpSelf._handleAudioExplorerKey(pEvent);
165
+ }
103
166
  else if (tmpActiveMode === 'viewer')
104
167
  {
105
168
  tmpSelf._handleViewerKey(pEvent);
@@ -156,9 +219,9 @@ class GalleryNavigationProvider extends libPictProvider
156
219
  this._toggleViewMode();
157
220
  break;
158
221
 
159
- case '/':
222
+ case 'x':
160
223
  pEvent.preventDefault();
161
- this._focusSearch();
224
+ this._clearAllFilters();
162
225
  break;
163
226
 
164
227
  case 'Home':
@@ -174,6 +237,8 @@ class GalleryNavigationProvider extends libPictProvider
174
237
  case 'f':
175
238
  pEvent.preventDefault();
176
239
  {
240
+ // Ensure the filter bar is visible first
241
+ this._showFilterBar();
177
242
  let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
178
243
  if (tmpGalleryView)
179
244
  {
@@ -185,11 +250,16 @@ class GalleryNavigationProvider extends libPictProvider
185
250
  case 's':
186
251
  pEvent.preventDefault();
187
252
  {
188
- let tmpSortSelect = document.getElementById('RetoldRemote-Gallery-Sort');
189
- if (tmpSortSelect)
253
+ // Ensure the filter bar is visible first
254
+ this._showFilterBar();
255
+ setTimeout(() =>
190
256
  {
191
- tmpSortSelect.focus();
192
- }
257
+ let tmpSortSelect = document.getElementById('RetoldRemote-Gallery-Sort');
258
+ if (tmpSortSelect)
259
+ {
260
+ tmpSortSelect.focus();
261
+ }
262
+ }, 50);
193
263
  }
194
264
  break;
195
265
 
@@ -202,6 +272,141 @@ class GalleryNavigationProvider extends libPictProvider
202
272
  pEvent.preventDefault();
203
273
  this._toggleDistractionFree();
204
274
  break;
275
+
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Handle keyboard events when the sidebar file list has focus.
281
+ */
282
+ _handleSidebarKey(pEvent)
283
+ {
284
+ let tmpRows = document.querySelectorAll('#Pict-FileBrowser-DetailRows .pict-fb-detail-row');
285
+ let tmpCount = tmpRows.length;
286
+
287
+ if (tmpCount === 0)
288
+ {
289
+ // Nothing in the sidebar, bail back to gallery
290
+ this._blurSidebar();
291
+ return;
292
+ }
293
+
294
+ switch (pEvent.key)
295
+ {
296
+ case 'ArrowDown':
297
+ pEvent.preventDefault();
298
+ this._moveSidebarCursor(Math.min(this._sidebarCursorIndex + 1, tmpCount - 1));
299
+ break;
300
+
301
+ case 'ArrowUp':
302
+ pEvent.preventDefault();
303
+ this._moveSidebarCursor(Math.max(this._sidebarCursorIndex - 1, 0));
304
+ break;
305
+
306
+ case 'Home':
307
+ pEvent.preventDefault();
308
+ this._moveSidebarCursor(0);
309
+ break;
310
+
311
+ case 'End':
312
+ pEvent.preventDefault();
313
+ this._moveSidebarCursor(tmpCount - 1);
314
+ break;
315
+
316
+ case 'Enter':
317
+ pEvent.preventDefault();
318
+ {
319
+ // Click the focused row to open it (folder or file)
320
+ let tmpRow = tmpRows[this._sidebarCursorIndex];
321
+ if (tmpRow)
322
+ {
323
+ // Fire the dblclick handler which opens folders / selects files
324
+ let tmpDblClickHandler = tmpRow.getAttribute('ondblclick');
325
+ if (tmpDblClickHandler)
326
+ {
327
+ new Function(tmpDblClickHandler).call(tmpRow);
328
+ }
329
+ }
330
+ }
331
+ break;
332
+
333
+ case 'Escape':
334
+ pEvent.preventDefault();
335
+ this._blurSidebar();
336
+ break;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Move focus into the sidebar file list.
342
+ */
343
+ _focusSidebar()
344
+ {
345
+ let tmpWrap = document.querySelector('.content-editor-sidebar-wrap');
346
+ if (!tmpWrap || tmpWrap.classList.contains('collapsed'))
347
+ {
348
+ return;
349
+ }
350
+
351
+ this._sidebarFocused = true;
352
+ this._sidebarCursorIndex = 0;
353
+
354
+ // Apply visual focus ring on the sidebar
355
+ let tmpInner = document.querySelector('.content-editor-sidebar-inner');
356
+ if (tmpInner)
357
+ {
358
+ tmpInner.classList.add('keyboard-focused');
359
+ }
360
+
361
+ this._moveSidebarCursor(0);
362
+ }
363
+
364
+ /**
365
+ * Return focus from sidebar back to the gallery.
366
+ */
367
+ _blurSidebar()
368
+ {
369
+ this._sidebarFocused = false;
370
+
371
+ // Remove sidebar focus ring
372
+ let tmpInner = document.querySelector('.content-editor-sidebar-inner');
373
+ if (tmpInner)
374
+ {
375
+ tmpInner.classList.remove('keyboard-focused');
376
+ }
377
+
378
+ // Remove highlight from all rows
379
+ let tmpRows = document.querySelectorAll('#Pict-FileBrowser-DetailRows .pict-fb-detail-row');
380
+ for (let i = 0; i < tmpRows.length; i++)
381
+ {
382
+ tmpRows[i].classList.remove('sidebar-focused');
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Move the sidebar cursor to a new index and highlight the row.
388
+ */
389
+ _moveSidebarCursor(pIndex)
390
+ {
391
+ let tmpRows = document.querySelectorAll('#Pict-FileBrowser-DetailRows .pict-fb-detail-row');
392
+ if (tmpRows.length === 0)
393
+ {
394
+ return;
395
+ }
396
+
397
+ // Remove old highlight
398
+ if (this._sidebarCursorIndex < tmpRows.length)
399
+ {
400
+ tmpRows[this._sidebarCursorIndex].classList.remove('sidebar-focused');
401
+ }
402
+
403
+ this._sidebarCursorIndex = pIndex;
404
+
405
+ // Apply new highlight and scroll into view
406
+ if (pIndex < tmpRows.length)
407
+ {
408
+ tmpRows[pIndex].classList.add('sidebar-focused');
409
+ tmpRows[pIndex].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
205
410
  }
206
411
  }
207
412
 
@@ -265,6 +470,11 @@ class GalleryNavigationProvider extends libPictProvider
265
470
  this._cycleFitMode();
266
471
  break;
267
472
 
473
+ case 'Enter':
474
+ pEvent.preventDefault();
475
+ this._openWithVLC();
476
+ break;
477
+
268
478
  case 'd':
269
479
  pEvent.preventDefault();
270
480
  this._toggleDistractionFree();
@@ -272,6 +482,74 @@ class GalleryNavigationProvider extends libPictProvider
272
482
  }
273
483
  }
274
484
 
485
+ /**
486
+ * Handle keyboard events in video explorer mode.
487
+ */
488
+ _handleVideoExplorerKey(pEvent)
489
+ {
490
+ switch (pEvent.key)
491
+ {
492
+ case 'Escape':
493
+ pEvent.preventDefault();
494
+ let tmpVEX = this.pict.views['RetoldRemote-VideoExplorer'];
495
+ if (tmpVEX)
496
+ {
497
+ tmpVEX.goBack();
498
+ }
499
+ break;
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Handle keyboard events in audio explorer mode.
505
+ */
506
+ _handleAudioExplorerKey(pEvent)
507
+ {
508
+ let tmpAEX = this.pict.views['RetoldRemote-AudioExplorer'];
509
+ if (!tmpAEX)
510
+ {
511
+ return;
512
+ }
513
+
514
+ switch (pEvent.key)
515
+ {
516
+ case 'Escape':
517
+ pEvent.preventDefault();
518
+ if (tmpAEX._selectionStart >= 0)
519
+ {
520
+ tmpAEX.clearSelection();
521
+ }
522
+ else
523
+ {
524
+ tmpAEX.goBack();
525
+ }
526
+ break;
527
+ case '+':
528
+ case '=':
529
+ pEvent.preventDefault();
530
+ tmpAEX.zoomIn();
531
+ break;
532
+ case '-':
533
+ case '_':
534
+ pEvent.preventDefault();
535
+ tmpAEX.zoomOut();
536
+ break;
537
+ case '0':
538
+ pEvent.preventDefault();
539
+ tmpAEX.zoomToFit();
540
+ break;
541
+ case 'z':
542
+ case 'Z':
543
+ pEvent.preventDefault();
544
+ tmpAEX.zoomToSelection();
545
+ break;
546
+ case ' ':
547
+ pEvent.preventDefault();
548
+ tmpAEX.playSelection();
549
+ break;
550
+ }
551
+ }
552
+
275
553
  /**
276
554
  * Move the gallery cursor to a new index and update the UI.
277
555
  *
@@ -321,9 +599,9 @@ class GalleryNavigationProvider extends libPictProvider
321
599
 
322
600
  let tmpItem = tmpItems[tmpIndex];
323
601
 
324
- if (tmpItem.Type === 'folder')
602
+ if (tmpItem.Type === 'folder' || tmpItem.Type === 'archive')
325
603
  {
326
- // Navigate into the folder
604
+ // Navigate into the folder or archive
327
605
  let tmpApp = this.pict.PictApplication;
328
606
  if (tmpApp && tmpApp.loadFileList)
329
607
  {
@@ -353,7 +631,9 @@ class GalleryNavigationProvider extends libPictProvider
353
631
  return;
354
632
  }
355
633
 
356
- let tmpParent = tmpCurrentLocation.replace(/\/[^/]+\/?$/, '') || '';
634
+ let tmpParent = tmpCurrentLocation.indexOf('/') >= 0
635
+ ? tmpCurrentLocation.replace(/\/[^/]+\/?$/, '')
636
+ : '';
357
637
  let tmpApp = this.pict.PictApplication;
358
638
  if (tmpApp && tmpApp.loadFileList)
359
639
  {
@@ -467,6 +747,95 @@ class GalleryNavigationProvider extends libPictProvider
467
747
  }
468
748
  }
469
749
 
750
+ // ──────────────────────────────────────────────
751
+ // Filter bar toggle
752
+ // ──────────────────────────────────────────────
753
+
754
+ /**
755
+ * Toggle the filter bar visibility.
756
+ * If hidden, show it and focus the search input.
757
+ * If visible, hide it.
758
+ */
759
+ _toggleFilterBar()
760
+ {
761
+ let tmpRemote = this.pict.AppData.RetoldRemote;
762
+
763
+ if (tmpRemote.FilterBarVisible)
764
+ {
765
+ this._hideFilterBar();
766
+ }
767
+ else
768
+ {
769
+ this._showFilterBar();
770
+ }
771
+ }
772
+
773
+ /**
774
+ * Show the filter bar and focus the search input.
775
+ */
776
+ _showFilterBar()
777
+ {
778
+ let tmpRemote = this.pict.AppData.RetoldRemote;
779
+
780
+ if (tmpRemote.FilterBarVisible)
781
+ {
782
+ // Already visible — just focus search
783
+ let tmpSearch = document.getElementById('RetoldRemote-Gallery-Search');
784
+ if (tmpSearch)
785
+ {
786
+ tmpSearch.focus();
787
+ }
788
+ return;
789
+ }
790
+
791
+ tmpRemote.FilterBarVisible = true;
792
+
793
+ let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
794
+ if (tmpGalleryView)
795
+ {
796
+ tmpGalleryView.renderGallery();
797
+ }
798
+
799
+ // Focus the search input after render
800
+ setTimeout(() =>
801
+ {
802
+ let tmpSearch = document.getElementById('RetoldRemote-Gallery-Search');
803
+ if (tmpSearch)
804
+ {
805
+ tmpSearch.focus();
806
+ }
807
+ }, 50);
808
+ }
809
+
810
+ /**
811
+ * Hide the filter bar.
812
+ */
813
+ _hideFilterBar()
814
+ {
815
+ let tmpRemote = this.pict.AppData.RetoldRemote;
816
+ tmpRemote.FilterBarVisible = false;
817
+
818
+ let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
819
+ if (tmpGalleryView)
820
+ {
821
+ tmpGalleryView.renderGallery();
822
+ }
823
+ }
824
+
825
+ /**
826
+ * Clear all active filters and update the gallery.
827
+ */
828
+ _clearAllFilters()
829
+ {
830
+ let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
831
+ if (tmpGalleryView)
832
+ {
833
+ tmpGalleryView.clearAllFilters();
834
+ }
835
+
836
+ this._showToast('Filters cleared');
837
+ }
838
+
470
839
  // ──────────────────────────────────────────────
471
840
  // Help panel
472
841
  // ──────────────────────────────────────────────
@@ -522,11 +891,13 @@ class GalleryNavigationProvider extends libPictProvider
522
891
  ['← → ↑ ↓', 'Navigate tiles'],
523
892
  ['Enter', 'Open selected item'],
524
893
  ['Escape', 'Go up one folder'],
894
+ ['F9', 'Toggle sidebar focus'],
525
895
  ['Home / End', 'Jump to first / last'],
526
896
  ['g', 'Toggle gallery / list view'],
527
- ['/', 'Focus search bar'],
528
- ['f', 'Toggle filter panel'],
897
+ ['/', 'Toggle filter bar &amp; search'],
898
+ ['f', 'Toggle advanced filter panel'],
529
899
  ['s', 'Focus sort dropdown'],
900
+ ['x', 'Clear all filters'],
530
901
  ['c', 'Settings / config panel'],
531
902
  ['d', 'Distraction-free mode']
532
903
  ];
@@ -539,6 +910,26 @@ class GalleryNavigationProvider extends libPictProvider
539
910
  }
540
911
  tmpHTML += '</div>';
541
912
 
913
+ // Sidebar shortcuts
914
+ tmpHTML += '<div class="retold-remote-help-section">';
915
+ tmpHTML += '<div class="retold-remote-help-section-title">Sidebar (F9 to focus)</div>';
916
+
917
+ let tmpSidebarShortcuts =
918
+ [
919
+ ['↑ / ↓', 'Navigate file list'],
920
+ ['Enter', 'Open selected item'],
921
+ ['Home / End', 'Jump to first / last'],
922
+ ['Escape / F9', 'Return to gallery']
923
+ ];
924
+ for (let i = 0; i < tmpSidebarShortcuts.length; i++)
925
+ {
926
+ tmpHTML += '<div class="retold-remote-help-row">';
927
+ tmpHTML += '<kbd class="retold-remote-help-key">' + tmpSidebarShortcuts[i][0] + '</kbd>';
928
+ tmpHTML += '<span class="retold-remote-help-desc">' + tmpSidebarShortcuts[i][1] + '</span>';
929
+ tmpHTML += '</div>';
930
+ }
931
+ tmpHTML += '</div>';
932
+
542
933
  // Viewer shortcuts
543
934
  tmpHTML += '<div class="retold-remote-help-section">';
544
935
  tmpHTML += '<div class="retold-remote-help-section-title">Media Viewer</div>';
@@ -551,6 +942,7 @@ class GalleryNavigationProvider extends libPictProvider
551
942
  ['f', 'Toggle fullscreen'],
552
943
  ['i', 'Toggle file info'],
553
944
  ['Space', 'Play / pause media'],
945
+ ['Enter', 'Open video in VLC'],
554
946
  ['z', 'Cycle fit mode'],
555
947
  ['+ / -', 'Zoom in / out'],
556
948
  ['0', 'Reset zoom'],
@@ -572,6 +964,7 @@ class GalleryNavigationProvider extends libPictProvider
572
964
  let tmpGlobalShortcuts =
573
965
  [
574
966
  ['F1', 'Toggle this help panel'],
967
+ ['F9', 'Toggle sidebar focus'],
575
968
  ['Escape', 'Close help panel']
576
969
  ];
577
970
  for (let i = 0; i < tmpGlobalShortcuts.length; i++)
@@ -585,7 +978,11 @@ class GalleryNavigationProvider extends libPictProvider
585
978
 
586
979
  // Active mode indicator
587
980
  tmpHTML += '<div class="retold-remote-help-footer">';
588
- tmpHTML += 'Current mode: <strong>' + (tmpActiveMode === 'viewer' ? 'Media Viewer' : 'Gallery') + '</strong>';
981
+ let tmpModeLabel = 'Gallery';
982
+ if (tmpActiveMode === 'viewer') tmpModeLabel = 'Media Viewer';
983
+ else if (tmpActiveMode === 'video-explorer') tmpModeLabel = 'Video Explorer';
984
+ else if (tmpActiveMode === 'audio-explorer') tmpModeLabel = 'Audio Explorer';
985
+ tmpHTML += 'Current mode: <strong>' + tmpModeLabel + '</strong>';
589
986
  tmpHTML += '</div>';
590
987
 
591
988
  tmpHTML += '</div>'; // end flyout
@@ -595,7 +992,7 @@ class GalleryNavigationProvider extends libPictProvider
595
992
  }
596
993
 
597
994
  /**
598
- * F2 — Toggle the settings/configuration panel.
995
+ * F9 — Toggle the settings/configuration panel.
599
996
  * Opens the sidebar if collapsed, switches to the Settings tab,
600
997
  * or toggles back to the Files tab if Settings is already showing.
601
998
  */
@@ -736,14 +1133,27 @@ class GalleryNavigationProvider extends libPictProvider
736
1133
 
737
1134
  _toggleFullscreen()
738
1135
  {
739
- let tmpViewer = document.getElementById('RetoldRemote-Viewer-Container');
740
- if (!tmpViewer) return;
741
-
742
1136
  if (document.fullscreenElement)
743
1137
  {
744
1138
  document.exitFullscreen();
1139
+ return;
745
1140
  }
746
- else
1141
+
1142
+ // When viewing a video, fullscreen the video element itself
1143
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1144
+ if (tmpRemote.CurrentViewerMediaType === 'video')
1145
+ {
1146
+ let tmpVideo = document.getElementById('RetoldRemote-VideoPlayer');
1147
+ if (tmpVideo)
1148
+ {
1149
+ tmpVideo.requestFullscreen();
1150
+ return;
1151
+ }
1152
+ }
1153
+
1154
+ // For other media types, fullscreen the viewer container
1155
+ let tmpViewer = document.getElementById('RetoldRemote-Viewer-Container');
1156
+ if (tmpViewer)
747
1157
  {
748
1158
  tmpViewer.requestFullscreen();
749
1159
  }
@@ -812,6 +1222,95 @@ class GalleryNavigationProvider extends libPictProvider
812
1222
  tmpImageViewer.cycleFitMode();
813
1223
  }
814
1224
  }
1225
+
1226
+ /**
1227
+ * Open the current video file with VLC via the server endpoint.
1228
+ */
1229
+ _openWithVLC()
1230
+ {
1231
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1232
+
1233
+ // Only works for video files
1234
+ if (tmpRemote.CurrentViewerMediaType !== 'video')
1235
+ {
1236
+ return;
1237
+ }
1238
+
1239
+ // Check if VLC is available
1240
+ let tmpCapabilities = tmpRemote.ServerCapabilities || {};
1241
+ if (!tmpCapabilities.vlc)
1242
+ {
1243
+ return;
1244
+ }
1245
+
1246
+ let tmpFilePath = tmpRemote.CurrentViewerFile;
1247
+ if (!tmpFilePath)
1248
+ {
1249
+ return;
1250
+ }
1251
+
1252
+ // Show a brief toast
1253
+ this._showToast('Opening in VLC...');
1254
+
1255
+ // POST to the server to open the file
1256
+ fetch('/api/media/open',
1257
+ {
1258
+ method: 'POST',
1259
+ headers: { 'Content-Type': 'application/json' },
1260
+ body: JSON.stringify({ path: tmpFilePath })
1261
+ })
1262
+ .then((pResponse) =>
1263
+ {
1264
+ return pResponse.json();
1265
+ })
1266
+ .then((pData) =>
1267
+ {
1268
+ if (!pData.Success)
1269
+ {
1270
+ this._showToast('Failed to open: ' + (pData.Error || 'Unknown error'));
1271
+ }
1272
+ })
1273
+ .catch((pError) =>
1274
+ {
1275
+ this._showToast('Failed to open: ' + pError.message);
1276
+ });
1277
+ }
1278
+
1279
+ /**
1280
+ * Show a brief toast notification in the viewer.
1281
+ *
1282
+ * @param {string} pMessage - Text to display
1283
+ */
1284
+ _showToast(pMessage)
1285
+ {
1286
+ let tmpIndicator = document.getElementById('RetoldRemote-FitIndicator');
1287
+ if (!tmpIndicator)
1288
+ {
1289
+ tmpIndicator = document.createElement('div');
1290
+ tmpIndicator.id = 'RetoldRemote-FitIndicator';
1291
+ tmpIndicator.className = 'retold-remote-fit-indicator';
1292
+
1293
+ let tmpContainer = document.querySelector('.retold-remote-viewer-body');
1294
+ if (tmpContainer)
1295
+ {
1296
+ tmpContainer.appendChild(tmpIndicator);
1297
+ }
1298
+ }
1299
+
1300
+ tmpIndicator.textContent = pMessage;
1301
+ tmpIndicator.classList.add('visible');
1302
+
1303
+ if (this._toastTimeout)
1304
+ {
1305
+ clearTimeout(this._toastTimeout);
1306
+ }
1307
+
1308
+ let tmpSelf = this;
1309
+ this._toastTimeout = setTimeout(function ()
1310
+ {
1311
+ tmpIndicator.classList.remove('visible');
1312
+ }, 1500);
1313
+ }
815
1314
  }
816
1315
 
817
1316
  GalleryNavigationProvider.default_configuration = _DefaultProviderConfiguration;
@@ -174,8 +174,17 @@ class RetoldRemoteProvider extends libPictProvider
174
174
  */
175
175
  getContentURL(pPath)
176
176
  {
177
- // Always use the /content/ static route for actual file serving.
178
- // Encode each path segment individually to preserve directory separators.
177
+ // When hashed filenames is enabled, use the /content-hashed/<hash> route
178
+ // so the real file path is never exposed to the browser.
179
+ if (this.pict.AppData.RetoldRemote.HashedFilenames)
180
+ {
181
+ let tmpHash = this.getHashForPath(pPath);
182
+ if (tmpHash)
183
+ {
184
+ return '/content-hashed/' + tmpHash;
185
+ }
186
+ }
187
+ // Fallback: encode each path segment individually to preserve directory separators.
179
188
  let tmpSegments = pPath.split('/').map((pSeg) => encodeURIComponent(pSeg));
180
189
  return '/content/' + tmpSegments.join('/');
181
190
  }
@@ -91,6 +91,7 @@ const _FallbackExtensionMap =
91
91
  '.zip': 'file-archive', '.tar': 'file-archive', '.gz': 'file-archive',
92
92
  '.rar': 'file-archive', '.7z': 'file-archive', '.bz2': 'file-archive',
93
93
  '.xz': 'file-archive', '.tgz': 'file-archive',
94
+ '.cbz': 'file-archive', '.cbr': 'file-archive',
94
95
 
95
96
  // Audio
96
97
  '.mp3': 'file-audio', '.wav': 'file-audio', '.flac': 'file-audio',