retold-remote 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retold-remote",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Retold Remote - NAS media browser with gallery views and keyboard navigation",
5
5
  "main": "source/Pict-RetoldRemote-Bundle.js",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "pict-section-filebrowser": "^0.0.2",
38
38
  "pict-service-commandlineutility": "^1.0.19",
39
39
  "pict-view": "^1.0.67",
40
- "retold-content-system": "^1.0.4",
40
+ "retold-content-system": "^1.0.8",
41
41
  "yauzl": "^3.2.0"
42
42
  },
43
43
  "optionalDependencies": {
@@ -20,6 +20,7 @@ const libViewMediaViewer = require('./views/PictView-Remote-MediaViewer.js');
20
20
  const libViewImageViewer = require('./views/PictView-Remote-ImageViewer.js');
21
21
  const libViewVideoExplorer = require('./views/PictView-Remote-VideoExplorer.js');
22
22
  const libViewAudioExplorer = require('./views/PictView-Remote-AudioExplorer.js');
23
+ const libViewVLCSetup = require('./views/PictView-Remote-VLCSetup.js');
23
24
 
24
25
  // Application configuration
25
26
  const _DefaultConfiguration = require('./Pict-Application-RetoldRemote-Configuration.json');
@@ -51,6 +52,7 @@ class RetoldRemoteApplication extends libContentEditorApplication
51
52
  this.pict.addView('RetoldRemote-SettingsPanel', libViewSettingsPanel.default_configuration, libViewSettingsPanel);
52
53
  this.pict.addView('RetoldRemote-VideoExplorer', libViewVideoExplorer.default_configuration, libViewVideoExplorer);
53
54
  this.pict.addView('RetoldRemote-AudioExplorer', libViewAudioExplorer.default_configuration, libViewAudioExplorer);
55
+ this.pict.addView('RetoldRemote-VLCSetup', libViewVLCSetup.default_configuration, libViewVLCSetup);
54
56
 
55
57
  // Add new providers
56
58
  this.pict.addProvider('RetoldRemote-Provider', libProviderRetoldRemote.default_configuration, libProviderRetoldRemote);
@@ -78,6 +80,7 @@ class RetoldRemoteApplication extends libContentEditorApplication
78
80
  RawFileList: [], // Unfiltered server response
79
81
  GalleryItems: [], // Filtered+sorted file list (single source of truth)
80
82
  GalleryCursorIndex: 0, // Currently highlighted item
83
+ FolderCursorHistory: {}, // Map of folder path -> last cursor index
81
84
  GalleryFilter: 'all', // 'all', 'images', 'video', 'audio', 'documents'
82
85
  SearchQuery: '',
83
86
  SearchCaseSensitive: false,
@@ -356,6 +359,44 @@ class RetoldRemoteApplication extends libContentEditorApplication
356
359
  }
357
360
  }
358
361
 
362
+ /**
363
+ * Navigate to a file with an explicit media type override, bypassing
364
+ * extension-based detection.
365
+ *
366
+ * @param {string} pFilePath - Relative file path
367
+ * @param {string} pMediaType - 'image', 'video', 'audio', or 'text'
368
+ */
369
+ navigateToFileAs(pFilePath, pMediaType)
370
+ {
371
+ if (!pFilePath)
372
+ {
373
+ return;
374
+ }
375
+
376
+ let tmpRemote = this.pict.AppData.RetoldRemote;
377
+
378
+ // Update the hash
379
+ let tmpFragProvider = this.pict.providers['RetoldRemote-Provider'];
380
+ let tmpFragId = tmpFragProvider ? tmpFragProvider.getFragmentIdentifier(pFilePath) : pFilePath;
381
+ window.location.hash = '#/view/' + tmpFragId;
382
+
383
+ // Update parent state for compatibility
384
+ this.pict.AppData.ContentEditor.CurrentFile = pFilePath;
385
+ this.pict.AppData.ContentEditor.ActiveEditor = 'binary';
386
+
387
+ let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
388
+ if (tmpViewer)
389
+ {
390
+ tmpViewer.showMedia(pFilePath, pMediaType);
391
+ }
392
+
393
+ let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
394
+ if (tmpTopBar)
395
+ {
396
+ tmpTopBar.updateInfo();
397
+ }
398
+ }
399
+
359
400
  /**
360
401
  * Override loadFileList to also populate the gallery and fetch folder summary.
361
402
  */
@@ -467,7 +508,8 @@ class RetoldRemoteApplication extends libContentEditorApplication
467
508
  {
468
509
  // Fallback if provider not ready
469
510
  tmpRemote.GalleryItems = pFileList || [];
470
- tmpRemote.GalleryCursorIndex = 0;
511
+ let tmpSavedIndex = tmpRemote.FolderCursorHistory && tmpRemote.FolderCursorHistory[tmpSelf.pict.AppData.PictFileBrowser.CurrentLocation || ''];
512
+ tmpRemote.GalleryCursorIndex = (typeof tmpSavedIndex === 'number' && tmpSavedIndex < (pFileList || []).length) ? tmpSavedIndex : 0;
471
513
  let tmpGalleryView = tmpSelf.pict.views['RetoldRemote-Gallery'];
472
514
  if (tmpGalleryView)
473
515
  {
@@ -1,7 +1,7 @@
1
1
  const libPictProvider = require('pict-provider');
2
2
 
3
3
  const _ImageExtensions = { 'png': true, 'jpg': true, 'jpeg': true, 'gif': true, 'webp': true, 'svg': true, 'bmp': true, 'ico': true, 'avif': true, 'tiff': true, 'tif': true };
4
- const _VideoExtensions = { 'mp4': true, 'webm': true, 'mov': true, 'mkv': true, 'avi': true, 'wmv': true, 'flv': true, 'm4v': true };
4
+ const _VideoExtensions = { 'mp4': true, 'webm': true, 'mov': true, 'mkv': true, 'avi': true, 'wmv': true, 'flv': true, 'm4v': true, 'ogv': true, 'mpg': true, 'mpeg': true, 'mpe': true, 'mpv': true, 'm2v': true, 'ts': true, 'mts': true, 'm2ts': true, 'vob': true, '3gp': true, '3g2': true, 'f4v': true, 'rm': true, 'rmvb': true, 'divx': true, 'asf': true, 'mxf': true, 'dv': true, 'nsv': true, 'nuv': true, 'y4m': true, 'wtv': true, 'swf': true, 'dat': true };
5
5
  const _AudioExtensions = { 'mp3': true, 'wav': true, 'ogg': true, 'flac': true, 'aac': true, 'm4a': true, 'wma': true };
6
6
  const _DocumentExtensions = { 'pdf': true, 'epub': true, 'mobi': true };
7
7
 
@@ -60,7 +60,18 @@ class GalleryFilterSortProvider extends libPictProvider
60
60
 
61
61
  // Write result
62
62
  tmpRemote.GalleryItems = tmpItems;
63
- tmpRemote.GalleryCursorIndex = 0;
63
+
64
+ // Restore cursor position if we have a saved one for this folder
65
+ let tmpCurrentLocation = (this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation) || '';
66
+ let tmpSavedIndex = tmpRemote.FolderCursorHistory && tmpRemote.FolderCursorHistory[tmpCurrentLocation];
67
+ if (typeof tmpSavedIndex === 'number' && tmpSavedIndex < tmpItems.length)
68
+ {
69
+ tmpRemote.GalleryCursorIndex = tmpSavedIndex;
70
+ }
71
+ else
72
+ {
73
+ tmpRemote.GalleryCursorIndex = 0;
74
+ }
64
75
 
65
76
  // Re-render gallery
66
77
  let tmpGalleryView = this.pict.views['RetoldRemote-Gallery'];
@@ -234,6 +234,26 @@ class GalleryNavigationProvider extends libPictProvider
234
234
  this.moveCursor(tmpItems.length - 1);
235
235
  break;
236
236
 
237
+ case '1':
238
+ pEvent.preventDefault();
239
+ this.openCurrentAs('image');
240
+ break;
241
+
242
+ case '2':
243
+ pEvent.preventDefault();
244
+ this.openCurrentAs('video');
245
+ break;
246
+
247
+ case '3':
248
+ pEvent.preventDefault();
249
+ this.openCurrentAs('audio');
250
+ break;
251
+
252
+ case '4':
253
+ pEvent.preventDefault();
254
+ this.openCurrentAs('text');
255
+ break;
256
+
237
257
  case 'f':
238
258
  pEvent.preventDefault();
239
259
  {
@@ -415,6 +435,66 @@ class GalleryNavigationProvider extends libPictProvider
415
435
  */
416
436
  _handleViewerKey(pEvent)
417
437
  {
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
+
418
498
  switch (pEvent.key)
419
499
  {
420
500
  case 'Escape':
@@ -472,13 +552,38 @@ class GalleryNavigationProvider extends libPictProvider
472
552
 
473
553
  case 'Enter':
474
554
  pEvent.preventDefault();
475
- this._openWithVLC();
555
+ this._streamWithVLC();
556
+ break;
557
+
558
+ case 'v':
559
+ pEvent.preventDefault();
560
+ this._streamWithVLC();
476
561
  break;
477
562
 
478
563
  case 'd':
479
564
  pEvent.preventDefault();
480
565
  this._toggleDistractionFree();
481
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;
482
587
  }
483
588
  }
484
589
 
@@ -601,6 +706,10 @@ class GalleryNavigationProvider extends libPictProvider
601
706
 
602
707
  if (tmpItem.Type === 'folder' || tmpItem.Type === 'archive')
603
708
  {
709
+ // Remember cursor position in the current folder before navigating away
710
+ let tmpCurrentLocation = (this.pict.AppData.PictFileBrowser && this.pict.AppData.PictFileBrowser.CurrentLocation) || '';
711
+ tmpRemote.FolderCursorHistory[tmpCurrentLocation] = tmpIndex;
712
+
604
713
  // Navigate into the folder or archive
605
714
  let tmpApp = this.pict.PictApplication;
606
715
  if (tmpApp && tmpApp.loadFileList)
@@ -619,6 +728,59 @@ class GalleryNavigationProvider extends libPictProvider
619
728
  }
620
729
  }
621
730
 
731
+ /**
732
+ * Open the currently selected gallery item, forcing a specific media type
733
+ * regardless of its file extension.
734
+ *
735
+ * @param {string} pMediaType - 'image', 'video', 'audio', or 'text'
736
+ */
737
+ openCurrentAs(pMediaType)
738
+ {
739
+ let tmpRemote = this.pict.AppData.RetoldRemote;
740
+ let tmpItems = tmpRemote.GalleryItems || [];
741
+ let tmpIndex = tmpRemote.GalleryCursorIndex || 0;
742
+
743
+ if (tmpIndex >= tmpItems.length)
744
+ {
745
+ return;
746
+ }
747
+
748
+ let tmpItem = tmpItems[tmpIndex];
749
+
750
+ if (tmpItem.Type === 'folder' || tmpItem.Type === 'archive')
751
+ {
752
+ return;
753
+ }
754
+
755
+ let tmpApp = this.pict.PictApplication;
756
+ if (tmpApp && tmpApp.navigateToFileAs)
757
+ {
758
+ tmpApp.navigateToFileAs(tmpItem.Path, pMediaType);
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Re-open the currently viewed file with a different media type.
764
+ *
765
+ * @param {string} pMediaType - 'image', 'video', 'audio', or 'text'
766
+ */
767
+ switchViewerType(pMediaType)
768
+ {
769
+ let tmpRemote = this.pict.AppData.RetoldRemote;
770
+ let tmpFilePath = tmpRemote.CurrentViewerFile;
771
+
772
+ if (!tmpFilePath)
773
+ {
774
+ return;
775
+ }
776
+
777
+ let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
778
+ if (tmpViewer)
779
+ {
780
+ tmpViewer.showMedia(tmpFilePath, pMediaType);
781
+ }
782
+ }
783
+
622
784
  /**
623
785
  * Navigate up one directory level.
624
786
  */
@@ -631,6 +793,10 @@ class GalleryNavigationProvider extends libPictProvider
631
793
  return;
632
794
  }
633
795
 
796
+ // Remember cursor position in the current folder before navigating away
797
+ let tmpRemote = this.pict.AppData.RetoldRemote;
798
+ tmpRemote.FolderCursorHistory[tmpCurrentLocation] = tmpRemote.GalleryCursorIndex || 0;
799
+
634
800
  let tmpParent = tmpCurrentLocation.indexOf('/') >= 0
635
801
  ? tmpCurrentLocation.replace(/\/[^/]+\/?$/, '')
636
802
  : '';
@@ -1276,6 +1442,47 @@ class GalleryNavigationProvider extends libPictProvider
1276
1442
  });
1277
1443
  }
1278
1444
 
1445
+ /**
1446
+ * Stream the current media file to VLC on the client device via vlc:// protocol link.
1447
+ */
1448
+ _streamWithVLC()
1449
+ {
1450
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1451
+ let tmpMediaType = tmpRemote.CurrentViewerMediaType;
1452
+
1453
+ if (tmpMediaType !== 'video' && tmpMediaType !== 'audio')
1454
+ {
1455
+ return;
1456
+ }
1457
+
1458
+ let tmpFilePath = tmpRemote.CurrentViewerFile;
1459
+ if (!tmpFilePath)
1460
+ {
1461
+ return;
1462
+ }
1463
+
1464
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
1465
+ let tmpContentPath = tmpProvider ? tmpProvider.getContentURL(tmpFilePath) : ('/content/' + encodeURIComponent(tmpFilePath));
1466
+ let tmpStreamURL = window.location.origin + tmpContentPath;
1467
+ // On Windows, VLC's native handler expects the raw URL.
1468
+ // On macOS/Linux our custom handlers URL-decode, so we encode.
1469
+ let tmpIsWindows = /Windows/.test(navigator.userAgent);
1470
+ let tmpVLCURL = tmpIsWindows
1471
+ ? ('vlc://' + tmpStreamURL)
1472
+ : ('vlc://' + encodeURIComponent(tmpStreamURL));
1473
+
1474
+ this._showToast('Opening VLC...');
1475
+
1476
+ // Use a temporary anchor element to trigger the protocol handler
1477
+ // without navigating the current page away
1478
+ let tmpLink = document.createElement('a');
1479
+ tmpLink.href = tmpVLCURL;
1480
+ tmpLink.style.display = 'none';
1481
+ document.body.appendChild(tmpLink);
1482
+ tmpLink.click();
1483
+ document.body.removeChild(tmpLink);
1484
+ }
1485
+
1279
1486
  /**
1280
1487
  * Show a brief toast notification in the viewer.
1281
1488
  *
@@ -19,7 +19,7 @@ const libToolDetector = require('./RetoldRemote-ToolDetector.js');
19
19
  const libThumbnailCache = require('./RetoldRemote-ThumbnailCache.js');
20
20
 
21
21
  const _ImageExtensions = { 'png': true, 'jpg': true, 'jpeg': true, 'gif': true, 'webp': true, 'svg': true, 'bmp': true, 'ico': true, 'avif': true, 'tiff': true, 'tif': true, 'heic': true, 'heif': true };
22
- const _VideoExtensions = { 'mp4': true, 'webm': true, 'mov': true, 'mkv': true, 'avi': true, 'wmv': true, 'flv': true, 'm4v': true, 'ogv': true };
22
+ const _VideoExtensions = { 'mp4': true, 'webm': true, 'mov': true, 'mkv': true, 'avi': true, 'wmv': true, 'flv': true, 'm4v': true, 'ogv': true, 'mpg': true, 'mpeg': true, 'mpe': true, 'mpv': true, 'm2v': true, 'ts': true, 'mts': true, 'm2ts': true, 'vob': true, '3gp': true, '3g2': true, 'f4v': true, 'rm': true, 'rmvb': true, 'divx': true, 'asf': true, 'mxf': true, 'dv': true, 'nsv': true, 'nuv': true, 'y4m': true, 'wtv': true, 'swf': true, 'dat': true };
23
23
  const _AudioExtensions = { 'mp3': true, 'wav': true, 'ogg': true, 'flac': true, 'aac': true, 'm4a': true, 'wma': true, 'oga': true };
24
24
  const _DocumentExtensions = { 'pdf': true, 'epub': true, 'mobi': true, 'doc': true, 'docx': true };
25
25
 
@@ -475,7 +475,7 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
475
475
  {
476
476
  let tmpOutputFormat = pFormat === 'webp' ? 'webp' : 'mjpeg';
477
477
  // Extract a frame at 10% into the video
478
- let tmpCmd = `ffmpeg -ss 00:00:02 -i "${pFullPath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -f image2 -c:v ${tmpOutputFormat} pipe:1`;
478
+ let tmpCmd = `ffmpeg -ss 00:00:02 -i "${pFullPath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -f ${tmpOutputFormat} pipe:1`;
479
479
  let tmpBuffer = libChildProcess.execSync(tmpCmd, { maxBuffer: 10 * 1024 * 1024, timeout: 30000 });
480
480
  return fCallback(null, tmpBuffer);
481
481
  }
@@ -147,7 +147,8 @@ class RetoldRemoteVideoFrameService extends libFableServiceProviderBase
147
147
 
148
148
  let tmpCodec = (pFormat === 'png') ? 'png' : (pFormat === 'webp') ? 'webp' : 'mjpeg';
149
149
 
150
- let tmpCmd = `ffmpeg -ss ${tmpTimeStr} -i "${pAbsPath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -c:v ${tmpCodec} -y "${pOutputPath}"`;
150
+ let tmpMuxer = (pFormat === 'png') ? 'image2' : (pFormat === 'webp') ? 'webp' : 'mjpeg';
151
+ let tmpCmd = `ffmpeg -ss ${tmpTimeStr} -i "${pAbsPath}" -vframes 1 -vf "scale=${pWidth}:${pHeight}:force_original_aspect_ratio=decrease" -f ${tmpMuxer} -y "${pOutputPath}"`;
151
152
  libChildProcess.execSync(tmpCmd, { stdio: 'ignore', timeout: 30000 });
152
153
  return libFs.existsSync(pOutputPath);
153
154
  }
@@ -340,6 +340,7 @@ class RetoldRemoteLayoutView extends libPictView
340
340
  tmpSettingsView.render();
341
341
  }
342
342
  }
343
+
343
344
  }
344
345
 
345
346
  _setupResizeHandle()