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
@@ -23,6 +23,14 @@ class RetoldRemoteImageExplorerView extends libPictView
23
23
  this._dziData = null;
24
24
  this._osdLoaded = false;
25
25
  this._loading = false;
26
+
27
+ // Selection mode state
28
+ this._selectionMode = false;
29
+ this._selectionTracker = null;
30
+ this._selectionOverlay = null;
31
+ this._selectionRegion = null; // { X, Y, Width, Height } in image coords
32
+ this._selectionStart = null; // viewport point where drag began
33
+ this._savedRegions = []; // loaded from server
26
34
  }
27
35
 
28
36
  /**
@@ -40,6 +48,14 @@ class RetoldRemoteImageExplorerView extends libPictView
40
48
  this._dziData = null;
41
49
  this._loading = false;
42
50
 
51
+ // Reset selection state
52
+ this._selectionMode = false;
53
+ this._selectionTracker = null;
54
+ this._selectionOverlay = null;
55
+ this._selectionRegion = null;
56
+ this._selectionStart = null;
57
+ this._savedRegions = [];
58
+
43
59
  // Clean up existing viewer
44
60
  if (this._osdViewer)
45
61
  {
@@ -89,6 +105,7 @@ class RetoldRemoteImageExplorerView extends libPictView
89
105
  tmpHTML += '<button class="retold-remote-iex-nav-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].goBack()" title="Back (Esc)">&larr; Back</button>';
90
106
  tmpHTML += '<div class="retold-remote-iex-title">Image Explorer &mdash; ' + tmpFmt.escapeHTML(tmpFileName) + '</div>';
91
107
  tmpHTML += '<div class="retold-remote-iex-actions">';
108
+ tmpHTML += '<button class="retold-remote-iex-action-btn" id="RetoldRemote-IEX-SelectBtn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].toggleSelectionMode()" title="Select a region (s)">&#9986; Select</button>';
92
109
  tmpHTML += '<button class="retold-remote-iex-action-btn" onclick="pict.views[\'RetoldRemote-ImageExplorer\'].viewInBrowser()" title="View in standard viewer">&#128444; View</button>';
93
110
  tmpHTML += '</div>';
94
111
  tmpHTML += '</div>';
@@ -111,6 +128,11 @@ class RetoldRemoteImageExplorerView extends libPictView
111
128
  tmpHTML += '<button onclick="pict.views[\'RetoldRemote-ImageExplorer\'].zoomOut()" title="Zoom Out (-)">- Zoom Out</button>';
112
129
  tmpHTML += '<button onclick="pict.views[\'RetoldRemote-ImageExplorer\'].zoomHome()" title="Fit to view (0)">Fit</button>';
113
130
  tmpHTML += '<span style="flex:1;"></span>';
131
+ tmpHTML += '<span id="RetoldRemote-IEX-LabelInput" style="display:none;">';
132
+ tmpHTML += '<input type="text" id="RetoldRemote-IEX-LabelField" placeholder="Label this region\u2026" style="background:var(--retold-bg-input,#1e1e1e);color:var(--retold-text,#abb2bf);border:1px solid var(--retold-border,#3e4451);border-radius:4px;padding:2px 8px;font-size:0.78rem;width:180px;margin-right:4px;" onkeydown="if(event.key===\'Enter\'){pict.views[\'RetoldRemote-ImageExplorer\'].saveSelectionLabel();event.preventDefault();event.stopPropagation();}if(event.key===\'Escape\'){pict.views[\'RetoldRemote-ImageExplorer\'].cancelSelection();event.preventDefault();event.stopPropagation();}">';
133
+ tmpHTML += '<button onclick="pict.views[\'RetoldRemote-ImageExplorer\'].saveSelectionLabel()" style="font-size:0.75rem;padding:2px 8px;">Save</button>';
134
+ tmpHTML += '<button onclick="pict.views[\'RetoldRemote-ImageExplorer\'].cancelSelection()" style="font-size:0.75rem;padding:2px 8px;margin-left:2px;">Cancel</button>';
135
+ tmpHTML += '</span>';
114
136
  tmpHTML += '<span id="RetoldRemote-IEX-Coords" style="color:var(--retold-text-dim);font-size:0.72rem;"></span>';
115
137
  tmpHTML += '</div>';
116
138
 
@@ -129,9 +151,11 @@ class RetoldRemoteImageExplorerView extends libPictView
129
151
  }
130
152
 
131
153
  // Load OpenSeadragon, then decide whether to use simple image or DZI tiles
154
+ let tmpSelfShow = this;
132
155
  this._ensureOSDLoaded(() =>
133
156
  {
134
- this._probeAndShow(pFilePath);
157
+ tmpSelfShow._probeAndShow(pFilePath);
158
+ tmpSelfShow._loadSavedRegions(pFilePath);
135
159
  });
136
160
  }
137
161
 
@@ -736,11 +760,592 @@ class RetoldRemoteImageExplorerView extends libPictView
736
760
  }
737
761
  }
738
762
 
763
+ // -----------------------------------------------------------------
764
+ // Selection mode — draw rectangles to create labeled subimage regions
765
+ // -----------------------------------------------------------------
766
+
767
+ /**
768
+ * Toggle selection mode on/off.
769
+ */
770
+ toggleSelectionMode()
771
+ {
772
+ if (this._selectionMode)
773
+ {
774
+ this._exitSelectionMode();
775
+ }
776
+ else
777
+ {
778
+ this._enterSelectionMode();
779
+ }
780
+ }
781
+
782
+ /**
783
+ * Enter selection mode: disable panning, install drag tracker.
784
+ */
785
+ _enterSelectionMode()
786
+ {
787
+ if (!this._osdViewer)
788
+ {
789
+ return;
790
+ }
791
+
792
+ this._selectionMode = true;
793
+
794
+ // Highlight the Select button
795
+ let tmpBtn = document.getElementById('RetoldRemote-IEX-SelectBtn');
796
+ if (tmpBtn)
797
+ {
798
+ tmpBtn.style.background = 'rgba(97, 175, 239, 0.4)';
799
+ tmpBtn.style.color = '#fff';
800
+ }
801
+
802
+ // Disable OSD panning so drag draws a selection instead
803
+ this._osdViewer.setMouseNavEnabled(false);
804
+
805
+ let tmpSelf = this;
806
+ let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
807
+ if (!tmpViewerDiv)
808
+ {
809
+ return;
810
+ }
811
+ tmpViewerDiv.style.cursor = 'crosshair';
812
+
813
+ this._selectionTracker = new OpenSeadragon.MouseTracker(
814
+ {
815
+ element: tmpViewerDiv,
816
+ pressHandler: function (pEvent)
817
+ {
818
+ tmpSelf._onSelectionPress(pEvent);
819
+ },
820
+ dragHandler: function (pEvent)
821
+ {
822
+ tmpSelf._onSelectionDrag(pEvent);
823
+ },
824
+ releaseHandler: function (pEvent)
825
+ {
826
+ tmpSelf._onSelectionRelease(pEvent);
827
+ }
828
+ });
829
+ }
830
+
831
+ /**
832
+ * Exit selection mode: re-enable panning, remove drag tracker.
833
+ */
834
+ _exitSelectionMode()
835
+ {
836
+ this._selectionMode = false;
837
+
838
+ let tmpBtn = document.getElementById('RetoldRemote-IEX-SelectBtn');
839
+ if (tmpBtn)
840
+ {
841
+ tmpBtn.style.background = '';
842
+ tmpBtn.style.color = '';
843
+ }
844
+
845
+ if (this._osdViewer)
846
+ {
847
+ this._osdViewer.setMouseNavEnabled(true);
848
+ }
849
+
850
+ if (this._selectionTracker)
851
+ {
852
+ this._selectionTracker.destroy();
853
+ this._selectionTracker = null;
854
+ }
855
+
856
+ let tmpViewerDiv = document.getElementById('RetoldRemote-IEX-Viewer');
857
+ if (tmpViewerDiv)
858
+ {
859
+ tmpViewerDiv.style.cursor = '';
860
+ }
861
+
862
+ // Remove in-progress selection overlay (keep saved ones)
863
+ this._removeActiveSelectionOverlay();
864
+ this._selectionRegion = null;
865
+ this._selectionStart = null;
866
+
867
+ // Hide label input
868
+ let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
869
+ if (tmpLabelWrap)
870
+ {
871
+ tmpLabelWrap.style.display = 'none';
872
+ }
873
+ let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
874
+ if (tmpCoords)
875
+ {
876
+ tmpCoords.style.display = '';
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Handle the start of a selection drag.
882
+ */
883
+ _onSelectionPress(pEvent)
884
+ {
885
+ if (!this._osdViewer)
886
+ {
887
+ return;
888
+ }
889
+
890
+ // Remove any previous in-progress selection overlay
891
+ this._removeActiveSelectionOverlay();
892
+
893
+ this._selectionStart = this._osdViewer.viewport.pointFromPixel(pEvent.position);
894
+
895
+ // Create the selection rectangle overlay element
896
+ let tmpOverlay = document.createElement('div');
897
+ tmpOverlay.id = 'RetoldRemote-IEX-ActiveSelection';
898
+ tmpOverlay.style.cssText = 'border: 2px solid rgba(97, 175, 239, 0.9); background: rgba(97, 175, 239, 0.15); pointer-events: none;';
899
+ this._selectionOverlay = tmpOverlay;
900
+
901
+ // Add the overlay at zero size, will expand during drag
902
+ this._osdViewer.addOverlay(
903
+ {
904
+ element: tmpOverlay,
905
+ location: new OpenSeadragon.Rect(
906
+ this._selectionStart.x, this._selectionStart.y, 0, 0)
907
+ });
908
+ }
909
+
910
+ /**
911
+ * Handle selection dragging — update the rectangle size.
912
+ */
913
+ _onSelectionDrag(pEvent)
914
+ {
915
+ if (!this._osdViewer || !this._selectionStart || !this._selectionOverlay)
916
+ {
917
+ return;
918
+ }
919
+
920
+ let tmpCurrent = this._osdViewer.viewport.pointFromPixel(pEvent.position);
921
+ let tmpX = Math.min(this._selectionStart.x, tmpCurrent.x);
922
+ let tmpY = Math.min(this._selectionStart.y, tmpCurrent.y);
923
+ let tmpW = Math.abs(tmpCurrent.x - this._selectionStart.x);
924
+ let tmpH = Math.abs(tmpCurrent.y - this._selectionStart.y);
925
+
926
+ this._osdViewer.updateOverlay(
927
+ this._selectionOverlay,
928
+ new OpenSeadragon.Rect(tmpX, tmpY, tmpW, tmpH));
929
+ }
930
+
931
+ /**
932
+ * Handle selection release — compute image-coordinate region and show label input.
933
+ */
934
+ _onSelectionRelease(pEvent)
935
+ {
936
+ if (!this._osdViewer || !this._selectionStart)
937
+ {
938
+ return;
939
+ }
940
+
941
+ let tmpEnd = this._osdViewer.viewport.pointFromPixel(pEvent.position);
942
+
943
+ // Convert viewport rectangle to image pixel coordinates
944
+ let tmpVpX = Math.min(this._selectionStart.x, tmpEnd.x);
945
+ let tmpVpY = Math.min(this._selectionStart.y, tmpEnd.y);
946
+ let tmpVpW = Math.abs(tmpEnd.x - this._selectionStart.x);
947
+ let tmpVpH = Math.abs(tmpEnd.y - this._selectionStart.y);
948
+
949
+ let tmpTopLeft = this._osdViewer.viewport.viewportToImageCoordinates(
950
+ new OpenSeadragon.Point(tmpVpX, tmpVpY));
951
+ let tmpBottomRight = this._osdViewer.viewport.viewportToImageCoordinates(
952
+ new OpenSeadragon.Point(tmpVpX + tmpVpW, tmpVpY + tmpVpH));
953
+
954
+ let tmpRegion =
955
+ {
956
+ X: Math.max(0, Math.round(tmpTopLeft.x)),
957
+ Y: Math.max(0, Math.round(tmpTopLeft.y)),
958
+ Width: Math.round(tmpBottomRight.x - tmpTopLeft.x),
959
+ Height: Math.round(tmpBottomRight.y - tmpTopLeft.y)
960
+ };
961
+
962
+ // Clamp to image dimensions
963
+ if (this._dziData)
964
+ {
965
+ if (tmpRegion.X + tmpRegion.Width > this._dziData.Width)
966
+ {
967
+ tmpRegion.Width = this._dziData.Width - tmpRegion.X;
968
+ }
969
+ if (tmpRegion.Y + tmpRegion.Height > this._dziData.Height)
970
+ {
971
+ tmpRegion.Height = this._dziData.Height - tmpRegion.Y;
972
+ }
973
+ }
974
+
975
+ // Ignore tiny selections (likely accidental clicks)
976
+ if (tmpRegion.Width < 5 || tmpRegion.Height < 5)
977
+ {
978
+ this._removeActiveSelectionOverlay();
979
+ return;
980
+ }
981
+
982
+ this._selectionRegion = tmpRegion;
983
+
984
+ // Show the inline label input in the controls bar
985
+ let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
986
+ let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
987
+ if (tmpLabelWrap)
988
+ {
989
+ tmpLabelWrap.style.display = '';
990
+ }
991
+ if (tmpCoords)
992
+ {
993
+ tmpCoords.style.display = 'none';
994
+ }
995
+
996
+ // Focus the label field
997
+ let tmpField = document.getElementById('RetoldRemote-IEX-LabelField');
998
+ if (tmpField)
999
+ {
1000
+ tmpField.value = '';
1001
+ tmpField.focus();
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * Save the current selection with the entered label.
1007
+ */
1008
+ saveSelectionLabel()
1009
+ {
1010
+ if (!this._selectionRegion)
1011
+ {
1012
+ return;
1013
+ }
1014
+
1015
+ let tmpField = document.getElementById('RetoldRemote-IEX-LabelField');
1016
+ let tmpLabel = tmpField ? tmpField.value.trim() : '';
1017
+
1018
+ let tmpSelf = this;
1019
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
1020
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._currentPath) : encodeURIComponent(this._currentPath);
1021
+
1022
+ fetch('/api/media/subimage-regions',
1023
+ {
1024
+ method: 'POST',
1025
+ headers: { 'Content-Type': 'application/json' },
1026
+ body: JSON.stringify(
1027
+ {
1028
+ Path: this._currentPath,
1029
+ Region:
1030
+ {
1031
+ Label: tmpLabel,
1032
+ X: this._selectionRegion.X,
1033
+ Y: this._selectionRegion.Y,
1034
+ Width: this._selectionRegion.Width,
1035
+ Height: this._selectionRegion.Height
1036
+ }
1037
+ })
1038
+ })
1039
+ .then((pResponse) => pResponse.json())
1040
+ .then((pResult) =>
1041
+ {
1042
+ if (pResult && pResult.Success)
1043
+ {
1044
+ tmpSelf._savedRegions = pResult.Regions || [];
1045
+
1046
+ // Remove the active selection overlay and render persistent ones
1047
+ tmpSelf._removeActiveSelectionOverlay();
1048
+ tmpSelf._renderSavedRegionOverlays();
1049
+
1050
+ // Notify the user
1051
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
1052
+ if (tmpToast)
1053
+ {
1054
+ tmpToast.showToast('Subimage region saved' + (tmpLabel ? ': ' + tmpLabel : ''));
1055
+ }
1056
+
1057
+ // Update the sidebar panel if visible
1058
+ let tmpSubPanel = tmpSelf.pict.views['RetoldRemote-SubimagesPanel'];
1059
+ if (tmpSubPanel)
1060
+ {
1061
+ tmpSubPanel.render();
1062
+ }
1063
+ }
1064
+ })
1065
+ .catch((pErr) =>
1066
+ {
1067
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
1068
+ if (tmpToast)
1069
+ {
1070
+ tmpToast.showToast('Failed to save region: ' + pErr.message);
1071
+ }
1072
+ });
1073
+
1074
+ // Hide label input, show coords
1075
+ this._selectionRegion = null;
1076
+ this._selectionStart = null;
1077
+
1078
+ let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
1079
+ if (tmpLabelWrap)
1080
+ {
1081
+ tmpLabelWrap.style.display = 'none';
1082
+ }
1083
+ let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
1084
+ if (tmpCoords)
1085
+ {
1086
+ tmpCoords.style.display = '';
1087
+ }
1088
+ }
1089
+
1090
+ /**
1091
+ * Cancel the current in-progress selection.
1092
+ */
1093
+ cancelSelection()
1094
+ {
1095
+ this._removeActiveSelectionOverlay();
1096
+ this._selectionRegion = null;
1097
+ this._selectionStart = null;
1098
+
1099
+ let tmpLabelWrap = document.getElementById('RetoldRemote-IEX-LabelInput');
1100
+ if (tmpLabelWrap)
1101
+ {
1102
+ tmpLabelWrap.style.display = 'none';
1103
+ }
1104
+ let tmpCoords = document.getElementById('RetoldRemote-IEX-Coords');
1105
+ if (tmpCoords)
1106
+ {
1107
+ tmpCoords.style.display = '';
1108
+ }
1109
+ }
1110
+
1111
+ /**
1112
+ * Remove the active (in-progress) selection overlay.
1113
+ */
1114
+ _removeActiveSelectionOverlay()
1115
+ {
1116
+ let tmpActive = document.getElementById('RetoldRemote-IEX-ActiveSelection');
1117
+ if (tmpActive && this._osdViewer)
1118
+ {
1119
+ try
1120
+ {
1121
+ this._osdViewer.removeOverlay(tmpActive);
1122
+ }
1123
+ catch (pErr)
1124
+ {
1125
+ // May not be an overlay; just remove from DOM
1126
+ if (tmpActive.parentElement)
1127
+ {
1128
+ tmpActive.parentElement.removeChild(tmpActive);
1129
+ }
1130
+ }
1131
+ }
1132
+ this._selectionOverlay = null;
1133
+ }
1134
+
1135
+ // -----------------------------------------------------------------
1136
+ // Saved region overlays
1137
+ // -----------------------------------------------------------------
1138
+
1139
+ /**
1140
+ * Load saved subimage regions from the server.
1141
+ *
1142
+ * @param {string} pFilePath - Relative file path
1143
+ */
1144
+ _loadSavedRegions(pFilePath)
1145
+ {
1146
+ let tmpSelf = this;
1147
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
1148
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
1149
+
1150
+ fetch('/api/media/subimage-regions?path=' + tmpPathParam)
1151
+ .then((pResponse) => pResponse.json())
1152
+ .then((pResult) =>
1153
+ {
1154
+ if (pResult && pResult.Success && Array.isArray(pResult.Regions))
1155
+ {
1156
+ tmpSelf._savedRegions = pResult.Regions;
1157
+ tmpSelf._renderSavedRegionOverlays();
1158
+ }
1159
+ })
1160
+ .catch(() =>
1161
+ {
1162
+ // Silently ignore — regions are optional
1163
+ });
1164
+ }
1165
+
1166
+ /**
1167
+ * Render all saved regions as OSD overlays with colored borders and labels.
1168
+ */
1169
+ _renderSavedRegionOverlays()
1170
+ {
1171
+ if (!this._osdViewer)
1172
+ {
1173
+ return;
1174
+ }
1175
+
1176
+ // Remove existing saved-region overlays
1177
+ let tmpExisting = document.querySelectorAll('.retold-remote-iex-region-overlay');
1178
+ for (let i = 0; i < tmpExisting.length; i++)
1179
+ {
1180
+ try
1181
+ {
1182
+ this._osdViewer.removeOverlay(tmpExisting[i]);
1183
+ }
1184
+ catch (pErr)
1185
+ {
1186
+ if (tmpExisting[i].parentElement)
1187
+ {
1188
+ tmpExisting[i].parentElement.removeChild(tmpExisting[i]);
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ // Render each saved region
1194
+ for (let i = 0; i < this._savedRegions.length; i++)
1195
+ {
1196
+ let tmpRegion = this._savedRegions[i];
1197
+ this._addRegionOverlay(tmpRegion);
1198
+ }
1199
+ }
1200
+
1201
+ /**
1202
+ * Add a single region overlay to the OSD viewer.
1203
+ *
1204
+ * @param {object} pRegion - { ID, Label, X, Y, Width, Height }
1205
+ */
1206
+ _addRegionOverlay(pRegion)
1207
+ {
1208
+ if (!this._osdViewer || !this._dziData)
1209
+ {
1210
+ return;
1211
+ }
1212
+
1213
+ let tmpEl = document.createElement('div');
1214
+ tmpEl.className = 'retold-remote-iex-region-overlay';
1215
+ tmpEl.setAttribute('data-region-id', pRegion.ID);
1216
+ tmpEl.style.cssText = 'border: 2px solid rgba(229, 192, 123, 0.85); background: rgba(229, 192, 123, 0.08); pointer-events: none; position: relative;';
1217
+
1218
+ // Label badge
1219
+ if (pRegion.Label)
1220
+ {
1221
+ let tmpLabelEl = document.createElement('span');
1222
+ tmpLabelEl.style.cssText = 'position:absolute;top:-1px;left:-1px;background:rgba(229,192,123,0.9);color:#282c34;font-size:0.65rem;padding:1px 5px;border-radius:0 0 3px 0;white-space:nowrap;pointer-events:none;';
1223
+ tmpLabelEl.textContent = pRegion.Label;
1224
+ tmpEl.appendChild(tmpLabelEl);
1225
+ }
1226
+
1227
+ // Convert image coordinates to viewport coordinates
1228
+ let tmpImageRect = new OpenSeadragon.Rect(pRegion.X, pRegion.Y, pRegion.Width, pRegion.Height);
1229
+ let tmpViewportRect = this._osdViewer.viewport.imageToViewportRectangle(tmpImageRect);
1230
+
1231
+ this._osdViewer.addOverlay(
1232
+ {
1233
+ element: tmpEl,
1234
+ location: tmpViewportRect
1235
+ });
1236
+ }
1237
+
1238
+ /**
1239
+ * Navigate to (zoom into) a specific saved region by ID.
1240
+ *
1241
+ * @param {string} pRegionID - The region ID to navigate to
1242
+ */
1243
+ zoomToRegion(pRegionID)
1244
+ {
1245
+ if (!this._osdViewer || !this._dziData)
1246
+ {
1247
+ return;
1248
+ }
1249
+
1250
+ let tmpRegion = null;
1251
+ for (let i = 0; i < this._savedRegions.length; i++)
1252
+ {
1253
+ if (this._savedRegions[i].ID === pRegionID)
1254
+ {
1255
+ tmpRegion = this._savedRegions[i];
1256
+ break;
1257
+ }
1258
+ }
1259
+
1260
+ if (!tmpRegion)
1261
+ {
1262
+ return;
1263
+ }
1264
+
1265
+ // Convert image rect to viewport rect and fit to it
1266
+ let tmpImageRect = new OpenSeadragon.Rect(tmpRegion.X, tmpRegion.Y, tmpRegion.Width, tmpRegion.Height);
1267
+ let tmpViewportRect = this._osdViewer.viewport.imageToViewportRectangle(tmpImageRect);
1268
+ this._osdViewer.viewport.fitBounds(tmpViewportRect);
1269
+ }
1270
+
1271
+ /**
1272
+ * Delete a saved region by ID.
1273
+ *
1274
+ * @param {string} pRegionID - The region ID to delete
1275
+ */
1276
+ deleteRegion(pRegionID)
1277
+ {
1278
+ let tmpSelf = this;
1279
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
1280
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._currentPath) : encodeURIComponent(this._currentPath);
1281
+
1282
+ fetch('/api/media/subimage-regions/' + encodeURIComponent(pRegionID) + '?path=' + tmpPathParam,
1283
+ {
1284
+ method: 'DELETE'
1285
+ })
1286
+ .then((pResponse) => pResponse.json())
1287
+ .then((pResult) =>
1288
+ {
1289
+ if (pResult && pResult.Success)
1290
+ {
1291
+ tmpSelf._savedRegions = pResult.Regions || [];
1292
+ tmpSelf._renderSavedRegionOverlays();
1293
+
1294
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
1295
+ if (tmpToast)
1296
+ {
1297
+ tmpToast.showToast('Region deleted');
1298
+ }
1299
+
1300
+ // Update sidebar
1301
+ let tmpSubPanel = tmpSelf.pict.views['RetoldRemote-SubimagesPanel'];
1302
+ if (tmpSubPanel)
1303
+ {
1304
+ tmpSubPanel.render();
1305
+ }
1306
+ }
1307
+ })
1308
+ .catch(() =>
1309
+ {
1310
+ // ignore
1311
+ });
1312
+ }
1313
+
1314
+ /**
1315
+ * Get the current selection region (for use by collection add).
1316
+ *
1317
+ * @returns {object|null} The current selection or null
1318
+ */
1319
+ getActiveSelection()
1320
+ {
1321
+ return this._selectionRegion;
1322
+ }
1323
+
1324
+ /**
1325
+ * Get the saved regions array.
1326
+ *
1327
+ * @returns {Array}
1328
+ */
1329
+ getSavedRegions()
1330
+ {
1331
+ return this._savedRegions;
1332
+ }
1333
+
1334
+ // -----------------------------------------------------------------
1335
+ // Navigation
1336
+ // -----------------------------------------------------------------
1337
+
739
1338
  /**
740
1339
  * Navigate back to the gallery / file listing.
741
1340
  */
742
1341
  goBack()
743
1342
  {
1343
+ // Clean up selection mode
1344
+ if (this._selectionMode)
1345
+ {
1346
+ this._exitSelectionMode();
1347
+ }
1348
+
744
1349
  // Destroy the OSD viewer
745
1350
  if (this._osdViewer)
746
1351
  {
@@ -68,11 +68,11 @@ class RetoldRemoteImageViewerView extends libPictView
68
68
  }
69
69
 
70
70
  /**
71
- * Show the explore button for opening the deep-zoom explorer.
71
+ * Show the explore button in the header nav bar.
72
72
  */
73
73
  _showExploreButton()
74
74
  {
75
- let tmpBtn = document.getElementById('RetoldRemote-ImageExploreBtn');
75
+ let tmpBtn = document.getElementById('RetoldRemote-HeaderExploreBtn');
76
76
  if (tmpBtn)
77
77
  {
78
78
  tmpBtn.style.display = '';
@@ -24,6 +24,7 @@ const _ViewConfiguration =
24
24
  <button class="content-editor-sidebar-tab active" data-tab="files" onclick="pict.views['ContentEditor-Layout'].switchSidebarTab('files')">Files</button>
25
25
  <button class="content-editor-sidebar-tab" data-tab="favorites" onclick="pict.views['ContentEditor-Layout'].switchSidebarTab('favorites')">Favorites</button>
26
26
  <button class="content-editor-sidebar-tab" data-tab="info" onclick="pict.views['ContentEditor-Layout'].switchSidebarTab('info')">Info</button>
27
+ <button class="content-editor-sidebar-tab" data-tab="subimages" onclick="pict.views['ContentEditor-Layout'].switchSidebarTab('subimages')">Regions</button>
27
28
  <button class="content-editor-sidebar-tab" data-tab="settings" onclick="pict.views['ContentEditor-Layout'].switchSidebarTab('settings')">Settings</button>
28
29
  <button class="content-editor-sidebar-tab content-editor-sidebar-tab-collections" data-tab="collections" onclick="pict.views['ContentEditor-Layout'].switchSidebarTab('collections')" style="display:none;">Collections</button>
29
30
  </div>
@@ -32,6 +33,7 @@ const _ViewConfiguration =
32
33
  <div id="RetoldRemote-Favorites-Body"></div>
33
34
  </div>
34
35
  <div class="content-editor-sidebar-pane" data-pane="info" id="RetoldRemote-Info-Container" style="display:none"></div>
36
+ <div class="content-editor-sidebar-pane" data-pane="subimages" id="RetoldRemote-Subimages-Container" style="display:none"></div>
35
37
  <div class="content-editor-sidebar-pane" data-pane="settings" id="RetoldRemote-Settings-Container" style="display:none"></div>
36
38
  <div class="content-editor-sidebar-pane" data-pane="collections" id="RetoldRemote-Collections-MobilePane" style="display:none"></div>
37
39
  </div>
@@ -196,6 +198,16 @@ class RetoldRemoteLayoutView extends libPictView
196
198
  pEl.style.display = (pEl.getAttribute('data-pane') === pTab) ? '' : 'none';
197
199
  });
198
200
 
201
+ // Subimages tab: render the subimages panel
202
+ if (pTab === 'subimages')
203
+ {
204
+ let tmpSubimagesView = this.pict.views['RetoldRemote-SubimagesPanel'];
205
+ if (tmpSubimagesView)
206
+ {
207
+ tmpSubimagesView.render();
208
+ }
209
+ }
210
+
199
211
  // Render settings panel on demand
200
212
  if (pTab === 'settings')
201
213
  {