retold-remote 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/css/retold-remote.css +3 -0
  2. package/docs/README.md +181 -0
  3. package/docs/_cover.md +14 -0
  4. package/docs/_sidebar.md +10 -0
  5. package/docs/_topbar.md +3 -0
  6. package/docs/audio-viewer.md +133 -0
  7. package/docs/ebook-reader.md +90 -0
  8. package/docs/image-viewer.md +90 -0
  9. package/docs/server-setup.md +262 -0
  10. package/docs/video-viewer.md +134 -0
  11. package/html/docs.html +59 -0
  12. package/html/index.html +1 -1
  13. package/package.json +20 -6
  14. package/source/Pict-Application-RetoldRemote.js +122 -2
  15. package/source/RetoldRemote-ExtensionMaps.js +33 -0
  16. package/source/cli/RetoldRemote-Server-Setup.js +211 -67
  17. package/source/cli/commands/RetoldRemote-Command-Serve.js +5 -26
  18. package/source/providers/Pict-Provider-AISortManager.js +456 -0
  19. package/source/providers/Pict-Provider-CollectionManager.js +1200 -0
  20. package/source/providers/Pict-Provider-FormattingUtilities.js +109 -0
  21. package/source/providers/Pict-Provider-GalleryFilterSort.js +2 -11
  22. package/source/providers/Pict-Provider-GalleryNavigation.js +187 -409
  23. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +52 -0
  24. package/source/providers/Pict-Provider-ToastNotification.js +96 -0
  25. package/source/providers/keyboard-handlers/KeyHandler-AudioExplorer.js +88 -0
  26. package/source/providers/keyboard-handlers/KeyHandler-Gallery.js +190 -0
  27. package/source/providers/keyboard-handlers/KeyHandler-Sidebar.js +65 -0
  28. package/source/providers/keyboard-handlers/KeyHandler-VideoExplorer.js +57 -0
  29. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +197 -0
  30. package/source/server/RetoldRemote-AISortService.js +879 -0
  31. package/source/server/RetoldRemote-ArchiveService.js +2 -12
  32. package/source/server/RetoldRemote-AudioWaveformService.js +7 -16
  33. package/source/server/RetoldRemote-CollectionService.js +843 -0
  34. package/source/server/RetoldRemote-EbookService.js +7 -16
  35. package/source/server/RetoldRemote-FileOperationService.js +560 -0
  36. package/source/server/RetoldRemote-MediaService.js +15 -14
  37. package/source/server/RetoldRemote-MetadataCache.js +411 -0
  38. package/source/server/RetoldRemote-ParimeCache.js +349 -0
  39. package/source/server/RetoldRemote-ThumbnailCache.js +52 -20
  40. package/source/server/RetoldRemote-VideoFrameService.js +7 -15
  41. package/source/views/PictView-Remote-AudioExplorer.js +10 -43
  42. package/source/views/PictView-Remote-CollectionsPanel.js +1486 -0
  43. package/source/views/PictView-Remote-Gallery.js +237 -44
  44. package/source/views/PictView-Remote-ImageViewer.js +1 -34
  45. package/source/views/PictView-Remote-Layout.js +411 -20
  46. package/source/views/PictView-Remote-MediaViewer.js +311 -28
  47. package/source/views/PictView-Remote-SettingsPanel.js +275 -133
  48. package/source/views/PictView-Remote-TopBar.js +701 -14
  49. package/source/views/PictView-Remote-VLCSetup.js +8 -39
  50. package/source/views/PictView-Remote-VideoExplorer.js +20 -54
  51. package/web-application/css/docuserve.css +73 -0
  52. package/web-application/css/retold-remote.css +3 -0
  53. package/web-application/docs/README.md +181 -0
  54. package/web-application/docs/_cover.md +14 -0
  55. package/web-application/docs/_sidebar.md +10 -0
  56. package/web-application/docs/_topbar.md +3 -0
  57. package/web-application/docs/audio-viewer.md +133 -0
  58. package/web-application/docs/ebook-reader.md +90 -0
  59. package/web-application/docs/image-viewer.md +90 -0
  60. package/web-application/docs/server-setup.md +262 -0
  61. package/web-application/docs/video-viewer.md +134 -0
  62. package/web-application/docs.html +59 -0
  63. package/web-application/index.html +1 -1
  64. package/web-application/js/pict-docuserve.min.js +58 -0
  65. package/web-application/js/pict.min.js +2 -2
  66. package/web-application/js/pict.min.js.map +1 -1
  67. package/web-application/retold-remote.js +2652 -970
  68. package/web-application/retold-remote.js.map +1 -1
  69. package/web-application/retold-remote.min.js +60 -13
  70. package/web-application/retold-remote.min.js.map +1 -1
  71. package/server.js +0 -43
@@ -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,129 +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;
221
-
222
- case 'x':
223
- pEvent.preventDefault();
224
- this._clearAllFilters();
225
- break;
226
-
227
- case 'Home':
228
- pEvent.preventDefault();
229
- this.moveCursor(0);
230
- break;
231
-
232
- case 'End':
233
- pEvent.preventDefault();
234
- this.moveCursor(tmpItems.length - 1);
235
- break;
236
-
237
- case '1':
238
- pEvent.preventDefault();
239
- this.openCurrentAs('image');
240
- break;
199
+ return;
200
+ }
241
201
 
242
- case '2':
243
- pEvent.preventDefault();
244
- this.openCurrentAs('video');
245
- 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
+ };
246
222
 
247
- case '3':
248
- pEvent.preventDefault();
249
- this.openCurrentAs('audio');
250
- break;
223
+ this._dfSwipeTouchEnd = function (pEvent)
224
+ {
225
+ if (tmpTouchCount !== 1)
226
+ {
227
+ return;
228
+ }
251
229
 
252
- case '4':
253
- pEvent.preventDefault();
254
- this.openCurrentAs('text');
255
- 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;
256
234
 
257
- case 'f':
258
- pEvent.preventDefault();
259
- {
260
- // Ensure the filter bar is visible first
261
- this._showFilterBar();
262
- let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
263
- if (tmpGalleryView)
264
- {
265
- tmpGalleryView.toggleFilterPanel();
266
- }
267
- }
268
- break;
235
+ // Must be primarily vertical
236
+ if (Math.abs(tmpDeltaY) < tmpSwipeThreshold || Math.abs(tmpDeltaX) > Math.abs(tmpDeltaY))
237
+ {
238
+ return;
239
+ }
269
240
 
270
- case 's':
271
- pEvent.preventDefault();
272
- {
273
- // Ensure the filter bar is visible first
274
- this._showFilterBar();
275
- setTimeout(() =>
276
- {
277
- let tmpSortSelect = document.getElementById('RetoldRemote-Gallery-Sort');
278
- if (tmpSortSelect)
279
- {
280
- tmpSortSelect.focus();
281
- }
282
- }, 50);
283
- }
284
- break;
241
+ let tmpRemote = tmpSelf.pict.AppData.RetoldRemote;
242
+ let tmpIsDF = tmpRemote._distractionFreeMode || false;
285
243
 
286
- case 'c':
287
- pEvent.preventDefault();
288
- this._toggleSettingsPanel();
289
- 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
+ };
290
255
 
291
- case 'd':
292
- pEvent.preventDefault();
293
- this._toggleDistractionFree();
294
- break;
256
+ document.addEventListener('touchstart', this._dfSwipeTouchStart, { passive: true });
257
+ document.addEventListener('touchend', this._dfSwipeTouchEnd, { passive: true });
258
+ this._dfSwipeBound = true;
259
+ }
295
260
 
296
- }
261
+ /**
262
+ * Handle keyboard events in gallery mode.
263
+ */
264
+ _handleGalleryKey(pEvent)
265
+ {
266
+ libHandleGalleryKey(this, pEvent);
297
267
  }
298
268
 
299
269
  /**
@@ -301,60 +271,7 @@ class GalleryNavigationProvider extends libPictProvider
301
271
  */
302
272
  _handleSidebarKey(pEvent)
303
273
  {
304
- let tmpRows = document.querySelectorAll('#Pict-FileBrowser-DetailRows .pict-fb-detail-row');
305
- let tmpCount = tmpRows.length;
306
-
307
- if (tmpCount === 0)
308
- {
309
- // Nothing in the sidebar, bail back to gallery
310
- this._blurSidebar();
311
- return;
312
- }
313
-
314
- switch (pEvent.key)
315
- {
316
- case 'ArrowDown':
317
- pEvent.preventDefault();
318
- this._moveSidebarCursor(Math.min(this._sidebarCursorIndex + 1, tmpCount - 1));
319
- break;
320
-
321
- case 'ArrowUp':
322
- pEvent.preventDefault();
323
- this._moveSidebarCursor(Math.max(this._sidebarCursorIndex - 1, 0));
324
- break;
325
-
326
- case 'Home':
327
- pEvent.preventDefault();
328
- this._moveSidebarCursor(0);
329
- break;
330
-
331
- case 'End':
332
- pEvent.preventDefault();
333
- this._moveSidebarCursor(tmpCount - 1);
334
- break;
335
-
336
- case 'Enter':
337
- pEvent.preventDefault();
338
- {
339
- // Click the focused row to open it (folder or file)
340
- let tmpRow = tmpRows[this._sidebarCursorIndex];
341
- if (tmpRow)
342
- {
343
- // Fire the dblclick handler which opens folders / selects files
344
- let tmpDblClickHandler = tmpRow.getAttribute('ondblclick');
345
- if (tmpDblClickHandler)
346
- {
347
- new Function(tmpDblClickHandler).call(tmpRow);
348
- }
349
- }
350
- }
351
- break;
352
-
353
- case 'Escape':
354
- pEvent.preventDefault();
355
- this._blurSidebar();
356
- break;
357
- }
274
+ libHandleSidebarKey(this, pEvent);
358
275
  }
359
276
 
360
277
  /**
@@ -435,156 +352,7 @@ class GalleryNavigationProvider extends libPictProvider
435
352
  */
436
353
  _handleViewerKey(pEvent)
437
354
  {
438
- let tmpRemote = this.pict.AppData.RetoldRemote;
439
-
440
- // Video action menu mode — intercept keys for menu options
441
- if (tmpRemote.VideoMenuActive && tmpRemote.CurrentViewerMediaType === 'video')
442
- {
443
- switch (pEvent.key)
444
- {
445
- case 'Escape':
446
- pEvent.preventDefault();
447
- this.closeViewer();
448
- return;
449
-
450
- case 'ArrowRight':
451
- case 'j':
452
- pEvent.preventDefault();
453
- this.nextFile();
454
- return;
455
-
456
- case 'ArrowLeft':
457
- case 'k':
458
- pEvent.preventDefault();
459
- this.prevFile();
460
- return;
461
-
462
- case 'e':
463
- pEvent.preventDefault();
464
- let tmpVEX = this.pict.views['RetoldRemote-VideoExplorer'];
465
- if (tmpVEX)
466
- {
467
- tmpVEX.showExplorer(tmpRemote.CurrentViewerFile);
468
- }
469
- return;
470
-
471
- case ' ':
472
- case 'Enter':
473
- pEvent.preventDefault();
474
- let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
475
- if (tmpViewer)
476
- {
477
- tmpViewer.playVideo();
478
- }
479
- return;
480
-
481
- case 't':
482
- pEvent.preventDefault();
483
- let tmpMediaViewer = this.pict.views['RetoldRemote-MediaViewer'];
484
- if (tmpMediaViewer)
485
- {
486
- tmpMediaViewer.loadVideoMenuFrame();
487
- }
488
- return;
489
-
490
- case 'v':
491
- pEvent.preventDefault();
492
- this._streamWithVLC();
493
- return;
494
- }
495
- return;
496
- }
497
-
498
- switch (pEvent.key)
499
- {
500
- case 'Escape':
501
- pEvent.preventDefault();
502
- this.closeViewer();
503
- break;
504
-
505
- case 'ArrowRight':
506
- case 'j':
507
- pEvent.preventDefault();
508
- this.nextFile();
509
- break;
510
-
511
- case 'ArrowLeft':
512
- case 'k':
513
- pEvent.preventDefault();
514
- this.prevFile();
515
- break;
516
-
517
- case 'f':
518
- pEvent.preventDefault();
519
- this._toggleFullscreen();
520
- break;
521
-
522
- case 'i':
523
- pEvent.preventDefault();
524
- this._toggleFileInfo();
525
- break;
526
-
527
- case ' ':
528
- pEvent.preventDefault();
529
- this._togglePlayPause();
530
- break;
531
-
532
- case '+':
533
- case '=':
534
- pEvent.preventDefault();
535
- this._zoomIn();
536
- break;
537
-
538
- case '-':
539
- pEvent.preventDefault();
540
- this._zoomOut();
541
- break;
542
-
543
- case '0':
544
- pEvent.preventDefault();
545
- this._zoomReset();
546
- break;
547
-
548
- case 'z':
549
- pEvent.preventDefault();
550
- this._cycleFitMode();
551
- break;
552
-
553
- case 'Enter':
554
- pEvent.preventDefault();
555
- this._streamWithVLC();
556
- break;
557
-
558
- case 'v':
559
- pEvent.preventDefault();
560
- this._streamWithVLC();
561
- break;
562
-
563
- case 'd':
564
- pEvent.preventDefault();
565
- this._toggleDistractionFree();
566
- break;
567
-
568
- case '1':
569
- pEvent.preventDefault();
570
- this.switchViewerType('image');
571
- break;
572
-
573
- case '2':
574
- pEvent.preventDefault();
575
- this.switchViewerType('video');
576
- break;
577
-
578
- case '3':
579
- pEvent.preventDefault();
580
- this.switchViewerType('audio');
581
- break;
582
-
583
- case '4':
584
- pEvent.preventDefault();
585
- this.switchViewerType('text');
586
- break;
587
- }
355
+ libHandleViewerKey(this, pEvent);
588
356
  }
589
357
 
590
358
  /**
@@ -592,17 +360,7 @@ class GalleryNavigationProvider extends libPictProvider
592
360
  */
593
361
  _handleVideoExplorerKey(pEvent)
594
362
  {
595
- switch (pEvent.key)
596
- {
597
- case 'Escape':
598
- pEvent.preventDefault();
599
- let tmpVEX = this.pict.views['RetoldRemote-VideoExplorer'];
600
- if (tmpVEX)
601
- {
602
- tmpVEX.goBack();
603
- }
604
- break;
605
- }
363
+ libHandleVideoExplorerKey(this, pEvent);
606
364
  }
607
365
 
608
366
  /**
@@ -610,49 +368,7 @@ class GalleryNavigationProvider extends libPictProvider
610
368
  */
611
369
  _handleAudioExplorerKey(pEvent)
612
370
  {
613
- let tmpAEX = this.pict.views['RetoldRemote-AudioExplorer'];
614
- if (!tmpAEX)
615
- {
616
- return;
617
- }
618
-
619
- switch (pEvent.key)
620
- {
621
- case 'Escape':
622
- pEvent.preventDefault();
623
- if (tmpAEX._selectionStart >= 0)
624
- {
625
- tmpAEX.clearSelection();
626
- }
627
- else
628
- {
629
- tmpAEX.goBack();
630
- }
631
- break;
632
- case '+':
633
- case '=':
634
- pEvent.preventDefault();
635
- tmpAEX.zoomIn();
636
- break;
637
- case '-':
638
- case '_':
639
- pEvent.preventDefault();
640
- tmpAEX.zoomOut();
641
- break;
642
- case '0':
643
- pEvent.preventDefault();
644
- tmpAEX.zoomToFit();
645
- break;
646
- case 'z':
647
- case 'Z':
648
- pEvent.preventDefault();
649
- tmpAEX.zoomToSelection();
650
- break;
651
- case ' ':
652
- pEvent.preventDefault();
653
- tmpAEX.playSelection();
654
- break;
655
- }
371
+ libHandleAudioExplorerKey(this, pEvent);
656
372
  }
657
373
 
658
374
  /**
@@ -686,6 +402,13 @@ class GalleryNavigationProvider extends libPictProvider
686
402
  // Scroll the tile into view if needed
687
403
  tmpNewTile.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
688
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
+ }
689
412
  }
690
413
 
691
414
  /**
@@ -815,12 +538,30 @@ class GalleryNavigationProvider extends libPictProvider
815
538
  let tmpRemote = this.pict.AppData.RetoldRemote;
816
539
  tmpRemote.ActiveMode = 'gallery';
817
540
 
541
+ // Exit collection browsing mode
542
+ tmpRemote.BrowsingCollection = false;
543
+ tmpRemote.BrowsingCollectionIndex = -1;
544
+
818
545
  let tmpGalleryContainer = document.getElementById('RetoldRemote-Gallery-Container');
819
546
  let tmpViewerContainer = document.getElementById('RetoldRemote-Viewer-Container');
820
547
 
821
548
  if (tmpGalleryContainer) tmpGalleryContainer.style.display = '';
822
549
  if (tmpViewerContainer) tmpViewerContainer.style.display = 'none';
823
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
+
824
565
  // Restore the hash to the browse route (use hashed identifier when available)
825
566
  let tmpCurrentLocation = (this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation) || '';
826
567
  let tmpFragProvider = this.pict.providers['RetoldRemote-Provider'];
@@ -837,10 +578,37 @@ class GalleryNavigationProvider extends libPictProvider
837
578
 
838
579
  /**
839
580
  * Navigate to the next file in the gallery list.
581
+ * When browsing a collection, navigates through collection items instead.
840
582
  */
841
583
  nextFile()
842
584
  {
843
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
+
844
612
  let tmpItems = tmpRemote.GalleryItems || [];
845
613
  let tmpIndex = tmpRemote.GalleryCursorIndex || 0;
846
614
 
@@ -862,10 +630,37 @@ class GalleryNavigationProvider extends libPictProvider
862
630
 
863
631
  /**
864
632
  * Navigate to the previous file in the gallery list.
633
+ * When browsing a collection, navigates through collection items instead.
865
634
  */
866
635
  prevFile()
867
636
  {
868
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
+
869
664
  let tmpItems = tmpRemote.GalleryItems || [];
870
665
  let tmpIndex = tmpRemote.GalleryCursorIndex || 0;
871
666
 
@@ -980,6 +775,7 @@ class GalleryNavigationProvider extends libPictProvider
980
775
  {
981
776
  let tmpRemote = this.pict.AppData.RetoldRemote;
982
777
  tmpRemote.FilterBarVisible = false;
778
+ tmpRemote.FilterPanelOpen = false;
983
779
 
984
780
  let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
985
781
  if (tmpGalleryView)
@@ -999,7 +795,7 @@ class GalleryNavigationProvider extends libPictProvider
999
795
  tmpGalleryView.clearAllFilters();
1000
796
  }
1001
797
 
1002
- this._showToast('Filters cleared');
798
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Filters cleared');
1003
799
  }
1004
800
 
1005
801
  // ──────────────────────────────────────────────
@@ -1065,7 +861,8 @@ class GalleryNavigationProvider extends libPictProvider
1065
861
  ['s', 'Focus sort dropdown'],
1066
862
  ['x', 'Clear all filters'],
1067
863
  ['c', 'Settings / config panel'],
1068
- ['d', 'Distraction-free mode']
864
+ ['d', 'Distraction-free mode'],
865
+ ['e', 'Video explorer (on video files)']
1069
866
  ];
1070
867
  for (let i = 0; i < tmpGalleryShortcuts.length; i++)
1071
868
  {
@@ -1142,6 +939,14 @@ class GalleryNavigationProvider extends libPictProvider
1142
939
  }
1143
940
  tmpHTML += '</div>';
1144
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
+
1145
950
  // Active mode indicator
1146
951
  tmpHTML += '<div class="retold-remote-help-footer">';
1147
952
  let tmpModeLabel = 'Gallery';
@@ -1293,6 +1098,13 @@ class GalleryNavigationProvider extends libPictProvider
1293
1098
  }
1294
1099
  }
1295
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
+
1296
1108
  // Recalculate gallery columns after layout change
1297
1109
  setTimeout(() => this.recalculateColumns(), 100);
1298
1110
  }
@@ -1330,7 +1142,8 @@ class GalleryNavigationProvider extends libPictProvider
1330
1142
  let tmpInfoOverlay = document.getElementById('RetoldRemote-FileInfo-Overlay');
1331
1143
  if (tmpInfoOverlay)
1332
1144
  {
1333
- tmpInfoOverlay.style.display = (tmpInfoOverlay.style.display === 'none') ? '' : 'none';
1145
+ let tmpIsHidden = window.getComputedStyle(tmpInfoOverlay).display === 'none';
1146
+ tmpInfoOverlay.style.display = tmpIsHidden ? 'block' : 'none';
1334
1147
  }
1335
1148
  }
1336
1149
 
@@ -1416,7 +1229,7 @@ class GalleryNavigationProvider extends libPictProvider
1416
1229
  }
1417
1230
 
1418
1231
  // Show a brief toast
1419
- this._showToast('Opening in VLC...');
1232
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Opening in VLC...');
1420
1233
 
1421
1234
  // POST to the server to open the file
1422
1235
  fetch('/api/media/open',
@@ -1433,12 +1246,12 @@ class GalleryNavigationProvider extends libPictProvider
1433
1246
  {
1434
1247
  if (!pData.Success)
1435
1248
  {
1436
- this._showToast('Failed to open: ' + (pData.Error || 'Unknown error'));
1249
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Failed to open: ' + (pData.Error || 'Unknown error'));
1437
1250
  }
1438
1251
  })
1439
1252
  .catch((pError) =>
1440
1253
  {
1441
- this._showToast('Failed to open: ' + pError.message);
1254
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Failed to open: ' + pError.message);
1442
1255
  });
1443
1256
  }
1444
1257
 
@@ -1471,7 +1284,7 @@ class GalleryNavigationProvider extends libPictProvider
1471
1284
  ? ('vlc://' + tmpStreamURL)
1472
1285
  : ('vlc://' + encodeURIComponent(tmpStreamURL));
1473
1286
 
1474
- this._showToast('Opening VLC...');
1287
+ this.pict.providers['RetoldRemote-ToastNotification'].showOverlayIndicator('Opening VLC...');
1475
1288
 
1476
1289
  // Use a temporary anchor element to trigger the protocol handler
1477
1290
  // without navigating the current page away
@@ -1483,41 +1296,6 @@ class GalleryNavigationProvider extends libPictProvider
1483
1296
  document.body.removeChild(tmpLink);
1484
1297
  }
1485
1298
 
1486
- /**
1487
- * Show a brief toast notification in the viewer.
1488
- *
1489
- * @param {string} pMessage - Text to display
1490
- */
1491
- _showToast(pMessage)
1492
- {
1493
- let tmpIndicator = document.getElementById('RetoldRemote-FitIndicator');
1494
- if (!tmpIndicator)
1495
- {
1496
- tmpIndicator = document.createElement('div');
1497
- tmpIndicator.id = 'RetoldRemote-FitIndicator';
1498
- tmpIndicator.className = 'retold-remote-fit-indicator';
1499
-
1500
- let tmpContainer = document.querySelector('.retold-remote-viewer-body');
1501
- if (tmpContainer)
1502
- {
1503
- tmpContainer.appendChild(tmpIndicator);
1504
- }
1505
- }
1506
-
1507
- tmpIndicator.textContent = pMessage;
1508
- tmpIndicator.classList.add('visible');
1509
-
1510
- if (this._toastTimeout)
1511
- {
1512
- clearTimeout(this._toastTimeout);
1513
- }
1514
-
1515
- let tmpSelf = this;
1516
- this._toastTimeout = setTimeout(function ()
1517
- {
1518
- tmpIndicator.classList.remove('visible');
1519
- }, 1500);
1520
- }
1521
1299
  }
1522
1300
 
1523
1301
  GalleryNavigationProvider.default_configuration = _DefaultProviderConfiguration;