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.
Files changed (63) hide show
  1. package/docs/README.md +181 -0
  2. package/docs/_cover.md +14 -0
  3. package/docs/_sidebar.md +10 -0
  4. package/docs/_topbar.md +3 -0
  5. package/docs/audio-viewer.md +133 -0
  6. package/docs/ebook-reader.md +90 -0
  7. package/docs/image-viewer.md +90 -0
  8. package/docs/server-setup.md +262 -0
  9. package/docs/video-viewer.md +134 -0
  10. package/html/docs.html +59 -0
  11. package/package.json +21 -7
  12. package/source/Pict-Application-RetoldRemote.js +143 -2
  13. package/source/RetoldRemote-ExtensionMaps.js +33 -0
  14. package/source/cli/RetoldRemote-Server-Setup.js +82 -67
  15. package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
  16. package/source/providers/Pict-Provider-CollectionManager.js +934 -0
  17. package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
  18. package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
  19. package/source/providers/Pict-Provider-GalleryNavigation.js +270 -353
  20. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
  21. package/source/providers/Pict-Provider-ToastNotification.js +96 -0
  22. package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
  23. package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
  24. package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
  25. package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
  26. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
  27. package/source/server/RetoldRemote-ArchiveService.js +2 -12
  28. package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
  29. package/source/server/RetoldRemote-CollectionService.js +684 -0
  30. package/source/server/RetoldRemote-EbookService.js +7 -16
  31. package/source/server/RetoldRemote-MediaService.js +3 -14
  32. package/source/server/RetoldRemote-ParimeCache.js +349 -0
  33. package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
  34. package/source/server/RetoldRemote-VideoFrameService.js +7 -15
  35. package/source/views/PictView-Remote-AudioExplorer.js +10 -43
  36. package/source/views/PictView-Remote-CollectionsPanel.js +1087 -0
  37. package/source/views/PictView-Remote-Gallery.js +237 -44
  38. package/source/views/PictView-Remote-ImageViewer.js +1 -34
  39. package/source/views/PictView-Remote-Layout.js +410 -20
  40. package/source/views/PictView-Remote-MediaViewer.js +338 -51
  41. package/source/views/PictView-Remote-SettingsPanel.js +155 -138
  42. package/source/views/PictView-Remote-TopBar.js +615 -14
  43. package/source/views/PictView-Remote-VLCSetup.js +766 -0
  44. package/source/views/PictView-Remote-VideoExplorer.js +20 -54
  45. package/web-application/css/docuserve.css +73 -0
  46. package/web-application/docs/README.md +181 -0
  47. package/web-application/docs/_cover.md +14 -0
  48. package/web-application/docs/_sidebar.md +10 -0
  49. package/web-application/docs/_topbar.md +3 -0
  50. package/web-application/docs/audio-viewer.md +133 -0
  51. package/web-application/docs/ebook-reader.md +90 -0
  52. package/web-application/docs/image-viewer.md +90 -0
  53. package/web-application/docs/server-setup.md +262 -0
  54. package/web-application/docs/video-viewer.md +134 -0
  55. package/web-application/docs.html +59 -0
  56. package/web-application/js/pict-docuserve.min.js +58 -0
  57. package/web-application/js/pict.min.js +2 -2
  58. package/web-application/js/pict.min.js.map +1 -1
  59. package/web-application/retold-remote.js +2558 -439
  60. package/web-application/retold-remote.js.map +1 -1
  61. package/web-application/retold-remote.min.js +41 -11
  62. package/web-application/retold-remote.min.js.map +1 -1
  63. package/server.js +0 -43
@@ -1,5 +1,11 @@
1
1
  const libPictProvider = require('pict-provider');
2
2
 
3
+ const libHandleGalleryKey = require('./keyboard-handlers/KeyHandler-Gallery.js');
4
+ const libHandleViewerKey = require('./keyboard-handlers/KeyHandler-Viewer.js');
5
+ const libHandleSidebarKey = require('./keyboard-handlers/KeyHandler-Sidebar.js');
6
+ const libHandleVideoExplorerKey = require('./keyboard-handlers/KeyHandler-VideoExplorer.js');
7
+ const libHandleAudioExplorerKey = require('./keyboard-handlers/KeyHandler-AudioExplorer.js');
8
+
3
9
  const _DefaultProviderConfiguration =
4
10
  {
5
11
  ProviderIdentifier: 'RetoldRemote-GalleryNavigation',
@@ -171,109 +177,93 @@ class GalleryNavigationProvider extends libPictProvider
171
177
 
172
178
  document.addEventListener('keydown', this._keydownHandler);
173
179
  this._keydownBound = true;
180
+
181
+ // Set up edge-swipe gestures for distraction-free mode on touch devices
182
+ this._setupDFSwipeGestures();
174
183
  }
175
184
 
176
185
  /**
177
- * Handle keyboard events in gallery mode.
186
+ * Set up touch swipe gestures near the top edge for toggling
187
+ * distraction-free mode.
188
+ *
189
+ * - Swipe UP starting within the top 60px of the viewport → enter DF
190
+ * - Swipe DOWN starting within the top 40px of the viewport → exit DF
191
+ *
192
+ * Only single-finger vertical swipes that exceed the threshold are
193
+ * recognised. Horizontal movement greater than vertical is ignored.
178
194
  */
179
- _handleGalleryKey(pEvent)
195
+ _setupDFSwipeGestures()
180
196
  {
181
- let tmpRemote = this.pict.AppData.RetoldRemote;
182
- let tmpItems = tmpRemote.GalleryItems || [];
183
- let tmpIndex = tmpRemote.GalleryCursorIndex || 0;
184
-
185
- switch (pEvent.key)
197
+ if (this._dfSwipeBound)
186
198
  {
187
- case 'ArrowRight':
188
- pEvent.preventDefault();
189
- this.moveCursor(Math.min(tmpIndex + 1, tmpItems.length - 1));
190
- break;
191
-
192
- case 'ArrowLeft':
193
- pEvent.preventDefault();
194
- this.moveCursor(Math.max(tmpIndex - 1, 0));
195
- break;
196
-
197
- case 'ArrowDown':
198
- pEvent.preventDefault();
199
- this.moveCursor(Math.min(tmpIndex + this._columnsPerRow, tmpItems.length - 1));
200
- break;
201
-
202
- case 'ArrowUp':
203
- pEvent.preventDefault();
204
- this.moveCursor(Math.max(tmpIndex - this._columnsPerRow, 0));
205
- break;
206
-
207
- case 'Enter':
208
- pEvent.preventDefault();
209
- this.openCurrent();
210
- break;
211
-
212
- case 'Escape':
213
- pEvent.preventDefault();
214
- this.navigateUp();
215
- break;
216
-
217
- case 'g':
218
- pEvent.preventDefault();
219
- this._toggleViewMode();
220
- break;
199
+ return;
200
+ }
221
201
 
222
- case 'x':
223
- pEvent.preventDefault();
224
- this._clearAllFilters();
225
- break;
202
+ let tmpSelf = this;
203
+ let tmpSwipeThreshold = 60;
204
+ let tmpEdgeEnter = 60;
205
+ let tmpEdgeExit = 40;
206
+ let tmpStartY = 0;
207
+ let tmpStartX = 0;
208
+ let tmpStartClientY = 0;
209
+ let tmpTouchCount = 0;
210
+
211
+ this._dfSwipeTouchStart = function (pEvent)
212
+ {
213
+ tmpTouchCount = pEvent.touches.length;
214
+ if (tmpTouchCount !== 1)
215
+ {
216
+ return;
217
+ }
218
+ tmpStartX = pEvent.touches[0].clientX;
219
+ tmpStartY = pEvent.touches[0].clientY;
220
+ tmpStartClientY = tmpStartY;
221
+ };
226
222
 
227
- case 'Home':
228
- pEvent.preventDefault();
229
- this.moveCursor(0);
230
- break;
223
+ this._dfSwipeTouchEnd = function (pEvent)
224
+ {
225
+ if (tmpTouchCount !== 1)
226
+ {
227
+ return;
228
+ }
231
229
 
232
- case 'End':
233
- pEvent.preventDefault();
234
- this.moveCursor(tmpItems.length - 1);
235
- break;
230
+ let tmpEndX = pEvent.changedTouches[0].clientX;
231
+ let tmpEndY = pEvent.changedTouches[0].clientY;
232
+ let tmpDeltaX = tmpEndX - tmpStartX;
233
+ let tmpDeltaY = tmpEndY - tmpStartY;
236
234
 
237
- case 'f':
238
- pEvent.preventDefault();
239
- {
240
- // Ensure the filter bar is visible first
241
- this._showFilterBar();
242
- let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
243
- if (tmpGalleryView)
244
- {
245
- tmpGalleryView.toggleFilterPanel();
246
- }
247
- }
248
- break;
235
+ // Must be primarily vertical
236
+ if (Math.abs(tmpDeltaY) < tmpSwipeThreshold || Math.abs(tmpDeltaX) > Math.abs(tmpDeltaY))
237
+ {
238
+ return;
239
+ }
249
240
 
250
- case 's':
251
- pEvent.preventDefault();
252
- {
253
- // Ensure the filter bar is visible first
254
- this._showFilterBar();
255
- setTimeout(() =>
256
- {
257
- let tmpSortSelect = document.getElementById('RetoldRemote-Gallery-Sort');
258
- if (tmpSortSelect)
259
- {
260
- tmpSortSelect.focus();
261
- }
262
- }, 50);
263
- }
264
- break;
241
+ let tmpRemote = tmpSelf.pict.AppData.RetoldRemote;
242
+ let tmpIsDF = tmpRemote._distractionFreeMode || false;
265
243
 
266
- case 'c':
267
- pEvent.preventDefault();
268
- this._toggleSettingsPanel();
269
- break;
244
+ if (!tmpIsDF && tmpDeltaY < 0 && tmpStartClientY <= tmpEdgeEnter)
245
+ {
246
+ // Swipe up from top edge → enter DF
247
+ tmpSelf._toggleDistractionFree();
248
+ }
249
+ else if (tmpIsDF && tmpDeltaY > 0 && tmpStartClientY <= tmpEdgeExit)
250
+ {
251
+ // Swipe down from top edge → exit DF
252
+ tmpSelf._toggleDistractionFree();
253
+ }
254
+ };
270
255
 
271
- case 'd':
272
- pEvent.preventDefault();
273
- this._toggleDistractionFree();
274
- break;
256
+ document.addEventListener('touchstart', this._dfSwipeTouchStart, { passive: true });
257
+ document.addEventListener('touchend', this._dfSwipeTouchEnd, { passive: true });
258
+ this._dfSwipeBound = true;
259
+ }
275
260
 
276
- }
261
+ /**
262
+ * Handle keyboard events in gallery mode.
263
+ */
264
+ _handleGalleryKey(pEvent)
265
+ {
266
+ libHandleGalleryKey(this, pEvent);
277
267
  }
278
268
 
279
269
  /**
@@ -281,60 +271,7 @@ class GalleryNavigationProvider extends libPictProvider
281
271
  */
282
272
  _handleSidebarKey(pEvent)
283
273
  {
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
- }
274
+ libHandleSidebarKey(this, pEvent);
338
275
  }
339
276
 
340
277
  /**
@@ -415,131 +352,7 @@ class GalleryNavigationProvider extends libPictProvider
415
352
  */
416
353
  _handleViewerKey(pEvent)
417
354
  {
418
- let tmpRemote = this.pict.AppData.RetoldRemote;
419
-
420
- // Video action menu mode — intercept keys for menu options
421
- if (tmpRemote.VideoMenuActive && tmpRemote.CurrentViewerMediaType === 'video')
422
- {
423
- switch (pEvent.key)
424
- {
425
- case 'Escape':
426
- pEvent.preventDefault();
427
- this.closeViewer();
428
- return;
429
-
430
- case 'ArrowRight':
431
- case 'j':
432
- pEvent.preventDefault();
433
- this.nextFile();
434
- return;
435
-
436
- case 'ArrowLeft':
437
- case 'k':
438
- pEvent.preventDefault();
439
- this.prevFile();
440
- return;
441
-
442
- case 'e':
443
- pEvent.preventDefault();
444
- let tmpVEX = this.pict.views['RetoldRemote-VideoExplorer'];
445
- if (tmpVEX)
446
- {
447
- tmpVEX.showExplorer(tmpRemote.CurrentViewerFile);
448
- }
449
- return;
450
-
451
- case ' ':
452
- case 'Enter':
453
- pEvent.preventDefault();
454
- let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
455
- if (tmpViewer)
456
- {
457
- tmpViewer.playVideo();
458
- }
459
- return;
460
-
461
- case 't':
462
- pEvent.preventDefault();
463
- let tmpMediaViewer = this.pict.views['RetoldRemote-MediaViewer'];
464
- if (tmpMediaViewer)
465
- {
466
- tmpMediaViewer.loadVideoMenuFrame();
467
- }
468
- return;
469
-
470
- case 'v':
471
- pEvent.preventDefault();
472
- this._openWithVLC();
473
- return;
474
- }
475
- return;
476
- }
477
-
478
- switch (pEvent.key)
479
- {
480
- case 'Escape':
481
- pEvent.preventDefault();
482
- this.closeViewer();
483
- break;
484
-
485
- case 'ArrowRight':
486
- case 'j':
487
- pEvent.preventDefault();
488
- this.nextFile();
489
- break;
490
-
491
- case 'ArrowLeft':
492
- case 'k':
493
- pEvent.preventDefault();
494
- this.prevFile();
495
- break;
496
-
497
- case 'f':
498
- pEvent.preventDefault();
499
- this._toggleFullscreen();
500
- break;
501
-
502
- case 'i':
503
- pEvent.preventDefault();
504
- this._toggleFileInfo();
505
- break;
506
-
507
- case ' ':
508
- pEvent.preventDefault();
509
- this._togglePlayPause();
510
- break;
511
-
512
- case '+':
513
- case '=':
514
- pEvent.preventDefault();
515
- this._zoomIn();
516
- break;
517
-
518
- case '-':
519
- pEvent.preventDefault();
520
- this._zoomOut();
521
- break;
522
-
523
- case '0':
524
- pEvent.preventDefault();
525
- this._zoomReset();
526
- break;
527
-
528
- case 'z':
529
- pEvent.preventDefault();
530
- this._cycleFitMode();
531
- break;
532
-
533
- case 'Enter':
534
- pEvent.preventDefault();
535
- this._openWithVLC();
536
- break;
537
-
538
- case 'd':
539
- pEvent.preventDefault();
540
- this._toggleDistractionFree();
541
- break;
542
- }
355
+ libHandleViewerKey(this, pEvent);
543
356
  }
544
357
 
545
358
  /**
@@ -547,17 +360,7 @@ class GalleryNavigationProvider extends libPictProvider
547
360
  */
548
361
  _handleVideoExplorerKey(pEvent)
549
362
  {
550
- switch (pEvent.key)
551
- {
552
- case 'Escape':
553
- pEvent.preventDefault();
554
- let tmpVEX = this.pict.views['RetoldRemote-VideoExplorer'];
555
- if (tmpVEX)
556
- {
557
- tmpVEX.goBack();
558
- }
559
- break;
560
- }
363
+ libHandleVideoExplorerKey(this, pEvent);
561
364
  }
562
365
 
563
366
  /**
@@ -565,49 +368,7 @@ class GalleryNavigationProvider extends libPictProvider
565
368
  */
566
369
  _handleAudioExplorerKey(pEvent)
567
370
  {
568
- let tmpAEX = this.pict.views['RetoldRemote-AudioExplorer'];
569
- if (!tmpAEX)
570
- {
571
- return;
572
- }
573
-
574
- switch (pEvent.key)
575
- {
576
- case 'Escape':
577
- pEvent.preventDefault();
578
- if (tmpAEX._selectionStart >= 0)
579
- {
580
- tmpAEX.clearSelection();
581
- }
582
- else
583
- {
584
- tmpAEX.goBack();
585
- }
586
- break;
587
- case '+':
588
- case '=':
589
- pEvent.preventDefault();
590
- tmpAEX.zoomIn();
591
- break;
592
- case '-':
593
- case '_':
594
- pEvent.preventDefault();
595
- tmpAEX.zoomOut();
596
- break;
597
- case '0':
598
- pEvent.preventDefault();
599
- tmpAEX.zoomToFit();
600
- break;
601
- case 'z':
602
- case 'Z':
603
- pEvent.preventDefault();
604
- tmpAEX.zoomToSelection();
605
- break;
606
- case ' ':
607
- pEvent.preventDefault();
608
- tmpAEX.playSelection();
609
- break;
610
- }
371
+ libHandleAudioExplorerKey(this, pEvent);
611
372
  }
612
373
 
613
374
  /**
@@ -641,6 +402,13 @@ class GalleryNavigationProvider extends libPictProvider
641
402
  // Scroll the tile into view if needed
642
403
  tmpNewTile.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
643
404
  }
405
+
406
+ // Update the top bar info to reflect the new cursor position
407
+ let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
408
+ if (tmpTopBar)
409
+ {
410
+ tmpTopBar.updateInfo();
411
+ }
644
412
  }
645
413
 
646
414
  /**
@@ -683,6 +451,59 @@ class GalleryNavigationProvider extends libPictProvider
683
451
  }
684
452
  }
685
453
 
454
+ /**
455
+ * Open the currently selected gallery item, forcing a specific media type
456
+ * regardless of its file extension.
457
+ *
458
+ * @param {string} pMediaType - 'image', 'video', 'audio', or 'text'
459
+ */
460
+ openCurrentAs(pMediaType)
461
+ {
462
+ let tmpRemote = this.pict.AppData.RetoldRemote;
463
+ let tmpItems = tmpRemote.GalleryItems || [];
464
+ let tmpIndex = tmpRemote.GalleryCursorIndex || 0;
465
+
466
+ if (tmpIndex >= tmpItems.length)
467
+ {
468
+ return;
469
+ }
470
+
471
+ let tmpItem = tmpItems[tmpIndex];
472
+
473
+ if (tmpItem.Type === 'folder' || tmpItem.Type === 'archive')
474
+ {
475
+ return;
476
+ }
477
+
478
+ let tmpApp = this.pict.PictApplication;
479
+ if (tmpApp && tmpApp.navigateToFileAs)
480
+ {
481
+ tmpApp.navigateToFileAs(tmpItem.Path, pMediaType);
482
+ }
483
+ }
484
+
485
+ /**
486
+ * Re-open the currently viewed file with a different media type.
487
+ *
488
+ * @param {string} pMediaType - 'image', 'video', 'audio', or 'text'
489
+ */
490
+ switchViewerType(pMediaType)
491
+ {
492
+ let tmpRemote = this.pict.AppData.RetoldRemote;
493
+ let tmpFilePath = tmpRemote.CurrentViewerFile;
494
+
495
+ if (!tmpFilePath)
496
+ {
497
+ return;
498
+ }
499
+
500
+ let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
501
+ if (tmpViewer)
502
+ {
503
+ tmpViewer.showMedia(tmpFilePath, pMediaType);
504
+ }
505
+ }
506
+
686
507
  /**
687
508
  * Navigate up one directory level.
688
509
  */
@@ -717,12 +538,30 @@ class GalleryNavigationProvider extends libPictProvider
717
538
  let tmpRemote = this.pict.AppData.RetoldRemote;
718
539
  tmpRemote.ActiveMode = 'gallery';
719
540
 
541
+ // Exit collection browsing mode
542
+ tmpRemote.BrowsingCollection = false;
543
+ tmpRemote.BrowsingCollectionIndex = -1;
544
+
720
545
  let tmpGalleryContainer = document.getElementById('RetoldRemote-Gallery-Container');
721
546
  let tmpViewerContainer = document.getElementById('RetoldRemote-Viewer-Container');
722
547
 
723
548
  if (tmpGalleryContainer) tmpGalleryContainer.style.display = '';
724
549
  if (tmpViewerContainer) tmpViewerContainer.style.display = 'none';
725
550
 
551
+ // Clean up swipe and DF exit listeners
552
+ let tmpMediaViewer = this.pict.views['RetoldRemote-MediaViewer'];
553
+ if (tmpMediaViewer)
554
+ {
555
+ if (tmpMediaViewer._cleanupSwipe)
556
+ {
557
+ tmpMediaViewer._cleanupSwipe();
558
+ }
559
+ if (tmpMediaViewer._cleanupDFExitHotspot)
560
+ {
561
+ tmpMediaViewer._cleanupDFExitHotspot();
562
+ }
563
+ }
564
+
726
565
  // Restore the hash to the browse route (use hashed identifier when available)
727
566
  let tmpCurrentLocation = (this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation) || '';
728
567
  let tmpFragProvider = this.pict.providers['RetoldRemote-Provider'];
@@ -739,10 +578,37 @@ class GalleryNavigationProvider extends libPictProvider
739
578
 
740
579
  /**
741
580
  * Navigate to the next file in the gallery list.
581
+ * When browsing a collection, navigates through collection items instead.
742
582
  */
743
583
  nextFile()
744
584
  {
745
585
  let tmpRemote = this.pict.AppData.RetoldRemote;
586
+
587
+ // Collection browsing mode — navigate through ActiveCollection.Items
588
+ if (tmpRemote.BrowsingCollection && tmpRemote.ActiveCollection)
589
+ {
590
+ let tmpCollItems = tmpRemote.ActiveCollection.Items || [];
591
+ let tmpCollIndex = tmpRemote.BrowsingCollectionIndex;
592
+
593
+ for (let i = tmpCollIndex + 1; i < tmpCollItems.length; i++)
594
+ {
595
+ let tmpItem = tmpCollItems[i];
596
+ if (tmpItem.Type === 'file' || tmpItem.Type === 'subfile' ||
597
+ tmpItem.Type === 'image-crop' || tmpItem.Type === 'video-clip' ||
598
+ tmpItem.Type === 'video-frame')
599
+ {
600
+ tmpRemote.BrowsingCollectionIndex = i;
601
+ let tmpApp = this.pict.PictApplication;
602
+ if (tmpApp && tmpApp.navigateToFile)
603
+ {
604
+ tmpApp.navigateToFile(tmpItem.Path);
605
+ }
606
+ return;
607
+ }
608
+ }
609
+ return;
610
+ }
611
+
746
612
  let tmpItems = tmpRemote.GalleryItems || [];
747
613
  let tmpIndex = tmpRemote.GalleryCursorIndex || 0;
748
614
 
@@ -764,10 +630,37 @@ class GalleryNavigationProvider extends libPictProvider
764
630
 
765
631
  /**
766
632
  * Navigate to the previous file in the gallery list.
633
+ * When browsing a collection, navigates through collection items instead.
767
634
  */
768
635
  prevFile()
769
636
  {
770
637
  let tmpRemote = this.pict.AppData.RetoldRemote;
638
+
639
+ // Collection browsing mode — navigate through ActiveCollection.Items
640
+ if (tmpRemote.BrowsingCollection && tmpRemote.ActiveCollection)
641
+ {
642
+ let tmpCollItems = tmpRemote.ActiveCollection.Items || [];
643
+ let tmpCollIndex = tmpRemote.BrowsingCollectionIndex;
644
+
645
+ for (let i = tmpCollIndex - 1; i >= 0; i--)
646
+ {
647
+ let tmpItem = tmpCollItems[i];
648
+ if (tmpItem.Type === 'file' || tmpItem.Type === 'subfile' ||
649
+ tmpItem.Type === 'image-crop' || tmpItem.Type === 'video-clip' ||
650
+ tmpItem.Type === 'video-frame')
651
+ {
652
+ tmpRemote.BrowsingCollectionIndex = i;
653
+ let tmpApp = this.pict.PictApplication;
654
+ if (tmpApp && tmpApp.navigateToFile)
655
+ {
656
+ tmpApp.navigateToFile(tmpItem.Path);
657
+ }
658
+ return;
659
+ }
660
+ }
661
+ return;
662
+ }
663
+
771
664
  let tmpItems = tmpRemote.GalleryItems || [];
772
665
  let tmpIndex = tmpRemote.GalleryCursorIndex || 0;
773
666
 
@@ -882,6 +775,7 @@ class GalleryNavigationProvider extends libPictProvider
882
775
  {
883
776
  let tmpRemote = this.pict.AppData.RetoldRemote;
884
777
  tmpRemote.FilterBarVisible = false;
778
+ tmpRemote.FilterPanelOpen = false;
885
779
 
886
780
  let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
887
781
  if (tmpGalleryView)
@@ -901,7 +795,7 @@ class GalleryNavigationProvider extends libPictProvider
901
795
  tmpGalleryView.clearAllFilters();
902
796
  }
903
797
 
904
- this._showToast('Filters cleared');
798
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Filters cleared');
905
799
  }
906
800
 
907
801
  // ──────────────────────────────────────────────
@@ -967,7 +861,8 @@ class GalleryNavigationProvider extends libPictProvider
967
861
  ['s', 'Focus sort dropdown'],
968
862
  ['x', 'Clear all filters'],
969
863
  ['c', 'Settings / config panel'],
970
- ['d', 'Distraction-free mode']
864
+ ['d', 'Distraction-free mode'],
865
+ ['e', 'Video explorer (on video files)']
971
866
  ];
972
867
  for (let i = 0; i < tmpGalleryShortcuts.length; i++)
973
868
  {
@@ -1044,6 +939,14 @@ class GalleryNavigationProvider extends libPictProvider
1044
939
  }
1045
940
  tmpHTML += '</div>';
1046
941
 
942
+ // Documentation link
943
+ tmpHTML += '<div class="retold-remote-help-section">';
944
+ tmpHTML += '<div class="retold-remote-help-row" style="justify-content: center; padding: 8px 0;">';
945
+ tmpHTML += '<a href="docs.html" target="_blank" style="color: var(--retold-accent, #569cd6); text-decoration: none; font-size: 0.9rem; cursor: pointer;">';
946
+ tmpHTML += 'View Documentation &#x2197;</a>';
947
+ tmpHTML += '</div>';
948
+ tmpHTML += '</div>';
949
+
1047
950
  // Active mode indicator
1048
951
  tmpHTML += '<div class="retold-remote-help-footer">';
1049
952
  let tmpModeLabel = 'Gallery';
@@ -1195,6 +1098,13 @@ class GalleryNavigationProvider extends libPictProvider
1195
1098
  }
1196
1099
  }
1197
1100
 
1101
+ // Sync DF toggle/hotspot in the media viewer
1102
+ let tmpMediaViewer = this.pict.views['RetoldRemote-MediaViewer'];
1103
+ if (tmpMediaViewer && tmpMediaViewer._updateDFControls)
1104
+ {
1105
+ tmpMediaViewer._updateDFControls();
1106
+ }
1107
+
1198
1108
  // Recalculate gallery columns after layout change
1199
1109
  setTimeout(() => this.recalculateColumns(), 100);
1200
1110
  }
@@ -1232,7 +1142,8 @@ class GalleryNavigationProvider extends libPictProvider
1232
1142
  let tmpInfoOverlay = document.getElementById('RetoldRemote-FileInfo-Overlay');
1233
1143
  if (tmpInfoOverlay)
1234
1144
  {
1235
- tmpInfoOverlay.style.display = (tmpInfoOverlay.style.display === 'none') ? '' : 'none';
1145
+ let tmpIsHidden = window.getComputedStyle(tmpInfoOverlay).display === 'none';
1146
+ tmpInfoOverlay.style.display = tmpIsHidden ? 'block' : 'none';
1236
1147
  }
1237
1148
  }
1238
1149
 
@@ -1318,7 +1229,7 @@ class GalleryNavigationProvider extends libPictProvider
1318
1229
  }
1319
1230
 
1320
1231
  // Show a brief toast
1321
- this._showToast('Opening in VLC...');
1232
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Opening in VLC...');
1322
1233
 
1323
1234
  // POST to the server to open the file
1324
1235
  fetch('/api/media/open',
@@ -1335,50 +1246,56 @@ class GalleryNavigationProvider extends libPictProvider
1335
1246
  {
1336
1247
  if (!pData.Success)
1337
1248
  {
1338
- this._showToast('Failed to open: ' + (pData.Error || 'Unknown error'));
1249
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Failed to open: ' + (pData.Error || 'Unknown error'));
1339
1250
  }
1340
1251
  })
1341
1252
  .catch((pError) =>
1342
1253
  {
1343
- this._showToast('Failed to open: ' + pError.message);
1254
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Failed to open: ' + pError.message);
1344
1255
  });
1345
1256
  }
1346
1257
 
1347
1258
  /**
1348
- * Show a brief toast notification in the viewer.
1349
- *
1350
- * @param {string} pMessage - Text to display
1259
+ * Stream the current media file to VLC on the client device via vlc:// protocol link.
1351
1260
  */
1352
- _showToast(pMessage)
1261
+ _streamWithVLC()
1353
1262
  {
1354
- let tmpIndicator = document.getElementById('RetoldRemote-FitIndicator');
1355
- if (!tmpIndicator)
1356
- {
1357
- tmpIndicator = document.createElement('div');
1358
- tmpIndicator.id = 'RetoldRemote-FitIndicator';
1359
- tmpIndicator.className = 'retold-remote-fit-indicator';
1263
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1264
+ let tmpMediaType = tmpRemote.CurrentViewerMediaType;
1360
1265
 
1361
- let tmpContainer = document.querySelector('.retold-remote-viewer-body');
1362
- if (tmpContainer)
1363
- {
1364
- tmpContainer.appendChild(tmpIndicator);
1365
- }
1266
+ if (tmpMediaType !== 'video' && tmpMediaType !== 'audio')
1267
+ {
1268
+ return;
1366
1269
  }
1367
1270
 
1368
- tmpIndicator.textContent = pMessage;
1369
- tmpIndicator.classList.add('visible');
1370
-
1371
- if (this._toastTimeout)
1271
+ let tmpFilePath = tmpRemote.CurrentViewerFile;
1272
+ if (!tmpFilePath)
1372
1273
  {
1373
- clearTimeout(this._toastTimeout);
1274
+ return;
1374
1275
  }
1375
1276
 
1376
- let tmpSelf = this;
1377
- this._toastTimeout = setTimeout(function ()
1378
- {
1379
- tmpIndicator.classList.remove('visible');
1380
- }, 1500);
1277
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
1278
+ let tmpContentPath = tmpProvider ? tmpProvider.getContentURL(tmpFilePath) : ('/content/' + encodeURIComponent(tmpFilePath));
1279
+ let tmpStreamURL = window.location.origin + tmpContentPath;
1280
+ // On Windows, VLC's native handler expects the raw URL.
1281
+ // On macOS/Linux our custom handlers URL-decode, so we encode.
1282
+ let tmpIsWindows = /Windows/.test(navigator.userAgent);
1283
+ let tmpVLCURL = tmpIsWindows
1284
+ ? ('vlc://' + tmpStreamURL)
1285
+ : ('vlc://' + encodeURIComponent(tmpStreamURL));
1286
+
1287
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Opening VLC...');
1288
+
1289
+ // Use a temporary anchor element to trigger the protocol handler
1290
+ // without navigating the current page away
1291
+ let tmpLink = document.createElement('a');
1292
+ tmpLink.href = tmpVLCURL;
1293
+ tmpLink.style.display = 'none';
1294
+ document.body.appendChild(tmpLink);
1295
+ tmpLink.click();
1296
+ document.body.removeChild(tmpLink);
1381
1297
  }
1298
+
1382
1299
  }
1383
1300
 
1384
1301
  GalleryNavigationProvider.default_configuration = _DefaultProviderConfiguration;