retold-remote 0.0.22 → 0.0.25

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 (47) hide show
  1. package/css/retold-remote.css +87 -20
  2. package/docs/README.md +59 -11
  3. package/docs/_sidebar.md +1 -0
  4. package/docs/collections.md +30 -0
  5. package/docs/ebook-reader.md +75 -1
  6. package/docs/image-explorer.md +27 -1
  7. package/docs/server-setup.md +28 -18
  8. package/docs/stack-launcher.md +218 -0
  9. package/docs/ultravisor-integration.md +2 -0
  10. package/package.json +10 -7
  11. package/source/Pict-Application-RetoldRemote.js +2 -0
  12. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  13. package/source/cli/RetoldRemote-Server-Setup.js +240 -2
  14. package/source/cli/RetoldRemote-Stack-Launcher.js +387 -0
  15. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  16. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  17. package/source/providers/CollectionManager-AddItems.js +166 -0
  18. package/source/providers/Pict-Provider-GalleryNavigation.js +46 -0
  19. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +5 -0
  20. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  21. package/source/server/RetoldRemote-CollectionExportService.js +696 -0
  22. package/source/server/RetoldRemote-CollectionService.js +5 -0
  23. package/source/server/RetoldRemote-EbookService.js +194 -3
  24. package/source/server/RetoldRemote-SubimageService.js +530 -0
  25. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  26. package/source/server/RetoldRemote-UltravisorOperations.js +6 -6
  27. package/source/views/MediaViewer-EbookViewer.js +419 -1
  28. package/source/views/MediaViewer-PdfViewer.js +963 -0
  29. package/source/views/PictView-Remote-CollectionsPanel.js +166 -0
  30. package/source/views/PictView-Remote-ImageExplorer.js +606 -1
  31. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  32. package/source/views/PictView-Remote-Layout.js +12 -0
  33. package/source/views/PictView-Remote-MediaViewer.js +83 -25
  34. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  35. package/web-application/css/retold-remote.css +87 -20
  36. package/web-application/docs/README.md +59 -11
  37. package/web-application/docs/_sidebar.md +1 -0
  38. package/web-application/docs/collections.md +30 -0
  39. package/web-application/docs/ebook-reader.md +75 -1
  40. package/web-application/docs/image-explorer.md +27 -1
  41. package/web-application/docs/server-setup.md +28 -18
  42. package/web-application/docs/stack-launcher.md +218 -0
  43. package/web-application/docs/ultravisor-integration.md +2 -0
  44. package/web-application/retold-remote.js +399 -45
  45. package/web-application/retold-remote.js.map +1 -1
  46. package/web-application/retold-remote.min.js +13 -12
  47. package/web-application/retold-remote.min.js.map +1 -1
@@ -2,7 +2,8 @@
2
2
  * MediaViewer — Ebook Viewer Mixin
3
3
  *
4
4
  * EPUB/MOBI rendering using epub.js, table of contents,
5
- * page navigation, and MOBI-to-EPUB server-side conversion.
5
+ * page navigation, text selection capture, visual rectangle
6
+ * selection, and MOBI-to-EPUB server-side conversion.
6
7
  *
7
8
  * Mixed into RetoldRemoteMediaViewerView.prototype via Object.assign().
8
9
  * All methods access state through `this` (the view instance).
@@ -33,6 +34,16 @@ module.exports =
33
34
  + '<button class="retold-remote-ebook-toc-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].toggleEbookTOC()">&#9776; TOC</button>'
34
35
  + '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookPrevPage()">&larr; Prev</button>'
35
36
  + '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookNextPage()">Next &rarr;</button>'
37
+ + '<span style="flex:1;"></span>'
38
+ + '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookSaveSelection()">&#128190; Save Selection</button>'
39
+ + '<button class="retold-remote-ebook-page-btn" id="RetoldRemote-EbookRegionSelectBtn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookToggleRegionSelect()">&#9986; Select Region</button>'
40
+ + '</div>'
41
+ + '<div class="retold-remote-ebook-controls" id="RetoldRemote-EbookLabelInput" style="display:none;">'
42
+ + '<input type="text" id="RetoldRemote-EbookLabelField" placeholder="Label..." '
43
+ + 'style="flex:1; padding:4px 8px; background:var(--retold-bg-secondary, #2d2d2d); color:var(--retold-text-primary, #d4d4d4); border:1px solid var(--retold-border, #444); border-radius:4px;" '
44
+ + 'onkeydown="if(event.key===\'Enter\'){pict.views[\'RetoldRemote-MediaViewer\'].ebookSaveLabel();}">'
45
+ + '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookSaveLabel()">Save</button>'
46
+ + '<button class="retold-remote-ebook-page-btn" onclick="pict.views[\'RetoldRemote-MediaViewer\'].ebookCancelSelection()">Cancel</button>'
36
47
  + '</div>'
37
48
  + '</div>'
38
49
  + '</div>';
@@ -288,5 +299,412 @@ module.exports =
288
299
  {
289
300
  tmpTocEl.classList.toggle('collapsed');
290
301
  }
302
+ },
303
+
304
+ /**
305
+ * Capture the current text selection from the epub.js rendition,
306
+ * derive a CFI, and show the label input for saving.
307
+ */
308
+ ebookSaveSelection: function ebookSaveSelection()
309
+ {
310
+ let tmpSelf = this;
311
+
312
+ if (!this._activeRendition)
313
+ {
314
+ this.pict.providers['RetoldRemote-ToastNotification'].showToast('No ebook loaded.');
315
+ return;
316
+ }
317
+
318
+ let tmpContents = this._activeRendition.getContents();
319
+ if (!tmpContents || tmpContents.length < 1)
320
+ {
321
+ this.pict.providers['RetoldRemote-ToastNotification'].showToast('Unable to access ebook contents.');
322
+ return;
323
+ }
324
+
325
+ let tmpDoc = tmpContents[0].document;
326
+ let tmpSelection = tmpDoc.getSelection();
327
+ let tmpSelectedText = tmpSelection ? tmpSelection.toString() : '';
328
+
329
+ if (!tmpSelectedText || tmpSelectedText.trim().length === 0)
330
+ {
331
+ this.pict.providers['RetoldRemote-ToastNotification'].showToast('Select text first.');
332
+ return;
333
+ }
334
+
335
+ // Derive the CFI from the selection range
336
+ let tmpCFI = '';
337
+ try
338
+ {
339
+ let tmpRange = tmpSelection.getRangeAt(0);
340
+ tmpCFI = tmpContents[0].cfiFromRange(tmpRange);
341
+ }
342
+ catch (pError)
343
+ {
344
+ this.pict.log.warn('Could not derive CFI from selection: ' + pError.message);
345
+ }
346
+
347
+ // Get current location for spine index
348
+ let tmpLocation = this._activeRendition.currentLocation();
349
+ let tmpSpineIndex = (tmpLocation && tmpLocation.start) ? tmpLocation.start.index : -1;
350
+
351
+ // Try to find the chapter title from the TOC
352
+ let tmpChapterTitle = '';
353
+ try
354
+ {
355
+ let tmpTocItems = document.querySelectorAll('#RetoldRemote-EbookTOCItems .retold-remote-ebook-toc-item');
356
+ if (tmpTocItems.length > 0 && tmpSpineIndex >= 0)
357
+ {
358
+ // Best effort: use the TOC item closest to the spine index
359
+ let tmpTocIndex = Math.min(tmpSpineIndex, tmpTocItems.length - 1);
360
+ tmpChapterTitle = tmpTocItems[tmpTocIndex].textContent.trim();
361
+ }
362
+ }
363
+ catch (pError)
364
+ {
365
+ // Chapter title is best-effort; ignore errors
366
+ }
367
+
368
+ // Store pending selection data on the view instance
369
+ this._pendingEbookSelection =
370
+ {
371
+ Type: 'text-selection',
372
+ CFI: tmpCFI,
373
+ SpineIndex: tmpSpineIndex,
374
+ ChapterTitle: tmpChapterTitle,
375
+ SelectedText: tmpSelectedText
376
+ };
377
+
378
+ // Show the label input
379
+ let tmpLabelInput = document.getElementById('RetoldRemote-EbookLabelInput');
380
+ if (tmpLabelInput)
381
+ {
382
+ tmpLabelInput.style.display = '';
383
+ }
384
+ let tmpLabelField = document.getElementById('RetoldRemote-EbookLabelField');
385
+ if (tmpLabelField)
386
+ {
387
+ tmpLabelField.value = '';
388
+ tmpLabelField.focus();
389
+ }
390
+ },
391
+
392
+ /**
393
+ * Toggle visual rectangle selection mode over the ebook content area.
394
+ * When enabled, an overlay captures mouse events to draw a rectangle.
395
+ */
396
+ ebookToggleRegionSelect: function ebookToggleRegionSelect()
397
+ {
398
+ let tmpSelf = this;
399
+ let tmpContentEl = document.getElementById('RetoldRemote-EbookContent');
400
+ let tmpToggleBtn = document.getElementById('RetoldRemote-EbookRegionSelectBtn');
401
+
402
+ if (!tmpContentEl)
403
+ {
404
+ return;
405
+ }
406
+
407
+ // If overlay already exists, remove it (toggle off)
408
+ let tmpExistingOverlay = document.getElementById('RetoldRemote-EbookRegionOverlay');
409
+ if (tmpExistingOverlay)
410
+ {
411
+ tmpExistingOverlay.remove();
412
+ if (tmpToggleBtn)
413
+ {
414
+ tmpToggleBtn.style.background = '';
415
+ }
416
+ this._ebookRegionActive = false;
417
+ return;
418
+ }
419
+
420
+ this._ebookRegionActive = true;
421
+ if (tmpToggleBtn)
422
+ {
423
+ tmpToggleBtn.style.background = 'var(--retold-accent, #569cd6)';
424
+ }
425
+
426
+ // Create a transparent overlay div
427
+ let tmpOverlay = document.createElement('div');
428
+ tmpOverlay.id = 'RetoldRemote-EbookRegionOverlay';
429
+ tmpOverlay.style.cssText = 'position:absolute; top:0; left:0; width:100%; height:100%; '
430
+ + 'cursor:crosshair; z-index:100; user-select:none;';
431
+ tmpContentEl.style.position = 'relative';
432
+ tmpContentEl.appendChild(tmpOverlay);
433
+
434
+ let tmpDrawing = false;
435
+ let tmpStartX = 0;
436
+ let tmpStartY = 0;
437
+ let tmpRectEl = null;
438
+
439
+ tmpOverlay.addEventListener('mousedown', function (pEvent)
440
+ {
441
+ pEvent.preventDefault();
442
+ pEvent.stopPropagation();
443
+
444
+ // Remove any previous rectangle
445
+ let tmpOldRect = document.getElementById('RetoldRemote-EbookRegionRect');
446
+ if (tmpOldRect)
447
+ {
448
+ tmpOldRect.remove();
449
+ }
450
+
451
+ let tmpBounds = tmpOverlay.getBoundingClientRect();
452
+ tmpStartX = pEvent.clientX - tmpBounds.left;
453
+ tmpStartY = pEvent.clientY - tmpBounds.top;
454
+ tmpDrawing = true;
455
+
456
+ tmpRectEl = document.createElement('div');
457
+ tmpRectEl.id = 'RetoldRemote-EbookRegionRect';
458
+ tmpRectEl.style.cssText = 'position:absolute; border:2px dashed var(--retold-accent, #569cd6); '
459
+ + 'background:rgba(86, 156, 214, 0.15); pointer-events:none;';
460
+ tmpRectEl.style.left = tmpStartX + 'px';
461
+ tmpRectEl.style.top = tmpStartY + 'px';
462
+ tmpRectEl.style.width = '0px';
463
+ tmpRectEl.style.height = '0px';
464
+ tmpOverlay.appendChild(tmpRectEl);
465
+ });
466
+
467
+ tmpOverlay.addEventListener('mousemove', function (pEvent)
468
+ {
469
+ if (!tmpDrawing || !tmpRectEl)
470
+ {
471
+ return;
472
+ }
473
+ pEvent.preventDefault();
474
+
475
+ let tmpBounds = tmpOverlay.getBoundingClientRect();
476
+ let tmpCurrentX = pEvent.clientX - tmpBounds.left;
477
+ let tmpCurrentY = pEvent.clientY - tmpBounds.top;
478
+
479
+ let tmpLeft = Math.min(tmpStartX, tmpCurrentX);
480
+ let tmpTop = Math.min(tmpStartY, tmpCurrentY);
481
+ let tmpWidth = Math.abs(tmpCurrentX - tmpStartX);
482
+ let tmpHeight = Math.abs(tmpCurrentY - tmpStartY);
483
+
484
+ tmpRectEl.style.left = tmpLeft + 'px';
485
+ tmpRectEl.style.top = tmpTop + 'px';
486
+ tmpRectEl.style.width = tmpWidth + 'px';
487
+ tmpRectEl.style.height = tmpHeight + 'px';
488
+ });
489
+
490
+ tmpOverlay.addEventListener('mouseup', function (pEvent)
491
+ {
492
+ if (!tmpDrawing || !tmpRectEl)
493
+ {
494
+ return;
495
+ }
496
+ pEvent.preventDefault();
497
+ tmpDrawing = false;
498
+
499
+ let tmpBounds = tmpOverlay.getBoundingClientRect();
500
+ let tmpEndX = pEvent.clientX - tmpBounds.left;
501
+ let tmpEndY = pEvent.clientY - tmpBounds.top;
502
+
503
+ let tmpRegionLeft = Math.min(tmpStartX, tmpEndX);
504
+ let tmpRegionTop = Math.min(tmpStartY, tmpEndY);
505
+ let tmpRegionWidth = Math.abs(tmpEndX - tmpStartX);
506
+ let tmpRegionHeight = Math.abs(tmpEndY - tmpStartY);
507
+
508
+ // Ignore tiny accidental drags
509
+ if (tmpRegionWidth < 5 || tmpRegionHeight < 5)
510
+ {
511
+ if (tmpRectEl)
512
+ {
513
+ tmpRectEl.remove();
514
+ tmpRectEl = null;
515
+ }
516
+ return;
517
+ }
518
+
519
+ let tmpViewportWidth = tmpOverlay.offsetWidth;
520
+ let tmpViewportHeight = tmpOverlay.offsetHeight;
521
+
522
+ // Best-effort text extraction from the rectangle area
523
+ let tmpExtractedText = '';
524
+ try
525
+ {
526
+ let tmpContents = tmpSelf._activeRendition.getContents();
527
+ if (tmpContents && tmpContents.length > 0)
528
+ {
529
+ let tmpDoc = tmpContents[0].document;
530
+ let tmpBody = tmpDoc.body;
531
+ if (tmpBody)
532
+ {
533
+ // Walk text nodes and check if any fall within the rectangle
534
+ let tmpTreeWalker = tmpDoc.createTreeWalker(tmpBody, NodeFilter.SHOW_TEXT, null, false);
535
+ let tmpTextParts = [];
536
+ let tmpNode;
537
+
538
+ while ((tmpNode = tmpTreeWalker.nextNode()))
539
+ {
540
+ if (!tmpNode.textContent || tmpNode.textContent.trim().length === 0)
541
+ {
542
+ continue;
543
+ }
544
+ let tmpRange = tmpDoc.createRange();
545
+ tmpRange.selectNodeContents(tmpNode);
546
+ let tmpRects = tmpRange.getClientRects();
547
+ for (let i = 0; i < tmpRects.length; i++)
548
+ {
549
+ let tmpR = tmpRects[i];
550
+ // Check overlap with the drawn rectangle
551
+ if (tmpR.right >= tmpRegionLeft && tmpR.left <= (tmpRegionLeft + tmpRegionWidth)
552
+ && tmpR.bottom >= tmpRegionTop && tmpR.top <= (tmpRegionTop + tmpRegionHeight))
553
+ {
554
+ tmpTextParts.push(tmpNode.textContent.trim());
555
+ break;
556
+ }
557
+ }
558
+ }
559
+ tmpExtractedText = tmpTextParts.join(' ');
560
+ }
561
+ }
562
+ }
563
+ catch (pError)
564
+ {
565
+ // Text extraction from region is best-effort
566
+ tmpSelf.pict.log.warn('Region text extraction failed: ' + pError.message);
567
+ }
568
+
569
+ // Get current location for spine index
570
+ let tmpLocation = tmpSelf._activeRendition.currentLocation();
571
+ let tmpSpineIndex = (tmpLocation && tmpLocation.start) ? tmpLocation.start.index : -1;
572
+
573
+ // Store pending selection data
574
+ tmpSelf._pendingEbookSelection =
575
+ {
576
+ Type: 'visual-region',
577
+ X: Math.round(tmpRegionLeft),
578
+ Y: Math.round(tmpRegionTop),
579
+ Width: Math.round(tmpRegionWidth),
580
+ Height: Math.round(tmpRegionHeight),
581
+ ViewportWidth: tmpViewportWidth,
582
+ ViewportHeight: tmpViewportHeight,
583
+ SpineIndex: tmpSpineIndex,
584
+ SelectedText: tmpExtractedText
585
+ };
586
+
587
+ // Show the label input
588
+ let tmpLabelInput = document.getElementById('RetoldRemote-EbookLabelInput');
589
+ if (tmpLabelInput)
590
+ {
591
+ tmpLabelInput.style.display = '';
592
+ }
593
+ let tmpLabelField = document.getElementById('RetoldRemote-EbookLabelField');
594
+ if (tmpLabelField)
595
+ {
596
+ tmpLabelField.value = '';
597
+ tmpLabelField.focus();
598
+ }
599
+ });
600
+ },
601
+
602
+ /**
603
+ * Cancel any in-progress selection or region, hide the label input,
604
+ * and remove the region overlay if present.
605
+ */
606
+ ebookCancelSelection: function ebookCancelSelection()
607
+ {
608
+ // Hide the label input
609
+ let tmpLabelInput = document.getElementById('RetoldRemote-EbookLabelInput');
610
+ if (tmpLabelInput)
611
+ {
612
+ tmpLabelInput.style.display = 'none';
613
+ }
614
+
615
+ // Clear the label field
616
+ let tmpLabelField = document.getElementById('RetoldRemote-EbookLabelField');
617
+ if (tmpLabelField)
618
+ {
619
+ tmpLabelField.value = '';
620
+ }
621
+
622
+ // Remove region overlay and rectangle if present
623
+ let tmpRect = document.getElementById('RetoldRemote-EbookRegionRect');
624
+ if (tmpRect)
625
+ {
626
+ tmpRect.remove();
627
+ }
628
+ let tmpOverlay = document.getElementById('RetoldRemote-EbookRegionOverlay');
629
+ if (tmpOverlay)
630
+ {
631
+ tmpOverlay.remove();
632
+ }
633
+
634
+ // Reset toggle button style
635
+ let tmpToggleBtn = document.getElementById('RetoldRemote-EbookRegionSelectBtn');
636
+ if (tmpToggleBtn)
637
+ {
638
+ tmpToggleBtn.style.background = '';
639
+ }
640
+
641
+ this._ebookRegionActive = false;
642
+ this._pendingEbookSelection = null;
643
+ },
644
+
645
+ /**
646
+ * Save the pending selection with the label from the input field.
647
+ * POSTs to /api/media/subimage-regions and updates the sidebar.
648
+ */
649
+ ebookSaveLabel: function ebookSaveLabel()
650
+ {
651
+ let tmpSelf = this;
652
+
653
+ if (!this._pendingEbookSelection)
654
+ {
655
+ this.pict.providers['RetoldRemote-ToastNotification'].showToast('No selection to save.');
656
+ return;
657
+ }
658
+
659
+ let tmpLabelField = document.getElementById('RetoldRemote-EbookLabelField');
660
+ let tmpLabelValue = tmpLabelField ? tmpLabelField.value.trim() : '';
661
+
662
+ if (!tmpLabelValue)
663
+ {
664
+ this.pict.providers['RetoldRemote-ToastNotification'].showToast('Enter a label for the selection.');
665
+ return;
666
+ }
667
+
668
+ let tmpRegion = this._pendingEbookSelection;
669
+ tmpRegion.Label = tmpLabelValue;
670
+
671
+ let tmpPayload =
672
+ {
673
+ Path: this.pict.AppData.RetoldRemote.CurrentViewerFile,
674
+ Region: tmpRegion
675
+ };
676
+
677
+ fetch('/api/media/subimage-regions',
678
+ {
679
+ method: 'POST',
680
+ headers: { 'Content-Type': 'application/json' },
681
+ body: JSON.stringify(tmpPayload)
682
+ })
683
+ .then((pResponse) =>
684
+ {
685
+ if (!pResponse.ok)
686
+ {
687
+ throw new Error('HTTP ' + pResponse.status);
688
+ }
689
+ return pResponse.json();
690
+ })
691
+ .then((pData) =>
692
+ {
693
+ tmpSelf.pict.providers['RetoldRemote-ToastNotification'].showToast('Selection saved: ' + tmpLabelValue);
694
+
695
+ // Clean up the selection state and UI
696
+ tmpSelf.ebookCancelSelection();
697
+
698
+ // Refresh the sidebar panel if a regions panel method exists
699
+ if (typeof (tmpSelf.refreshSubimageRegions) === 'function')
700
+ {
701
+ tmpSelf.refreshSubimageRegions();
702
+ }
703
+ })
704
+ .catch((pError) =>
705
+ {
706
+ tmpSelf.pict.providers['RetoldRemote-ToastNotification'].showToast('Failed to save selection: ' + pError.message);
707
+ tmpSelf.pict.log.error('Ebook selection save error: ' + pError.message);
708
+ });
291
709
  }
292
710
  };