retold-remote 0.0.23 → 0.0.26

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 (79) hide show
  1. package/css/retold-remote.css +343 -20
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +64 -12
  4. package/docs/_cover.md +6 -6
  5. package/docs/_sidebar.md +2 -0
  6. package/docs/_topbar.md +1 -1
  7. package/docs/_version.json +7 -0
  8. package/docs/collections.md +30 -0
  9. package/docs/css/docuserve.css +327 -0
  10. package/docs/ebook-reader.md +75 -1
  11. package/docs/image-explorer.md +62 -2
  12. package/docs/index.html +39 -0
  13. package/docs/retold-catalog.json +254 -0
  14. package/docs/retold-keyword-index.json +31216 -0
  15. package/docs/server-setup.md +122 -91
  16. package/docs/stack-launcher.md +218 -0
  17. package/docs/synology.md +585 -0
  18. package/docs/ultravisor-configuration.md +5 -5
  19. package/docs/ultravisor-integration.md +4 -2
  20. package/package.json +20 -14
  21. package/source/Pict-Application-RetoldRemote.js +22 -0
  22. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  23. package/source/cli/RetoldRemote-Server-Setup.js +460 -7
  24. package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
  25. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  26. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  27. package/source/providers/CollectionManager-AddItems.js +166 -0
  28. package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
  29. package/source/providers/Pict-Provider-OperationStatus.js +597 -0
  30. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
  31. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  32. package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
  33. package/source/server/RetoldRemote-CollectionExportService.js +763 -0
  34. package/source/server/RetoldRemote-CollectionService.js +5 -0
  35. package/source/server/RetoldRemote-EbookService.js +218 -3
  36. package/source/server/RetoldRemote-ImageService.js +221 -46
  37. package/source/server/RetoldRemote-MediaService.js +63 -4
  38. package/source/server/RetoldRemote-MetadataCache.js +25 -5
  39. package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
  40. package/source/server/RetoldRemote-SubimageService.js +680 -0
  41. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  42. package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
  43. package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
  44. package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
  45. package/source/server/RetoldRemote-VideoFrameService.js +302 -9
  46. package/source/views/MediaViewer-EbookViewer.js +419 -1
  47. package/source/views/MediaViewer-PdfViewer.js +1050 -0
  48. package/source/views/PictView-Remote-AudioExplorer.js +77 -1
  49. package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
  50. package/source/views/PictView-Remote-Gallery.js +365 -64
  51. package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
  52. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  53. package/source/views/PictView-Remote-Layout.js +58 -0
  54. package/source/views/PictView-Remote-MediaViewer.js +100 -25
  55. package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
  56. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  57. package/source/views/PictView-Remote-TopBar.js +1 -0
  58. package/source/views/PictView-Remote-VideoExplorer.js +77 -1
  59. package/web-application/css/docuserve.css +277 -23
  60. package/web-application/css/retold-remote.css +343 -20
  61. package/web-application/docs/README.md +64 -12
  62. package/web-application/docs/_cover.md +6 -6
  63. package/web-application/docs/_sidebar.md +2 -0
  64. package/web-application/docs/_topbar.md +1 -1
  65. package/web-application/docs/collections.md +30 -0
  66. package/web-application/docs/ebook-reader.md +75 -1
  67. package/web-application/docs/image-explorer.md +62 -2
  68. package/web-application/docs/server-setup.md +122 -91
  69. package/web-application/docs/stack-launcher.md +218 -0
  70. package/web-application/docs/synology.md +585 -0
  71. package/web-application/docs/ultravisor-configuration.md +5 -5
  72. package/web-application/docs/ultravisor-integration.md +4 -2
  73. package/web-application/js/pict-docuserve.min.js +12 -12
  74. package/web-application/js/pict.min.js +2 -2
  75. package/web-application/js/pict.min.js.map +1 -1
  76. package/web-application/retold-remote.js +6596 -1784
  77. package/web-application/retold-remote.js.map +1 -1
  78. package/web-application/retold-remote.min.js +75 -23
  79. package/web-application/retold-remote.min.js.map +1 -1
@@ -62,6 +62,14 @@ class RetoldRemoteAudioExplorerView extends libPictView
62
62
  tmpRemote.CurrentViewerFile = pFilePath;
63
63
  tmpRemote.CurrentViewerMediaType = 'audio';
64
64
  this._currentPath = pFilePath;
65
+
66
+ // Notify the layout so active sidebar panels (Info, Regions, etc.)
67
+ // refresh to the new file instead of keeping stale content.
68
+ let tmpLayout = this.pict.views['ContentEditor-Layout'];
69
+ if (tmpLayout && typeof tmpLayout.notifyCurrentFileChanged === 'function')
70
+ {
71
+ tmpLayout.notifyCurrentFileChanged(pFilePath);
72
+ }
65
73
  this._waveformData = null;
66
74
  this._peaks = [];
67
75
  this._viewStart = 0;
@@ -182,26 +190,91 @@ class RetoldRemoteAudioExplorerView extends libPictView
182
190
 
183
191
  let tmpURL = '/api/media/audio-waveform?path=' + tmpPathParam + '&peaks=2000';
184
192
 
185
- fetch(tmpURL)
193
+ // Cancel any previous in-flight waveform fetch
194
+ this._cancelActiveOperation();
195
+
196
+ // Start an operation entry so the user sees what's happening
197
+ let tmpStatus = this.pict.providers['RetoldRemote-OperationStatus'];
198
+ let tmpOp = tmpStatus ? tmpStatus.startOperation(
199
+ {
200
+ Label: 'Analyzing audio waveform',
201
+ Phase: 'Extracting peaks…',
202
+ Cancelable: true
203
+ }) : null;
204
+ if (tmpOp)
205
+ {
206
+ this._activeOperationId = tmpOp.OperationId;
207
+ this._activeAbortController = tmpOp.AbortController;
208
+ }
209
+
210
+ let tmpFetchOptions = {};
211
+ if (tmpOp && tmpOp.AbortController)
212
+ {
213
+ tmpFetchOptions.signal = tmpOp.AbortController.signal;
214
+ }
215
+ if (tmpOp)
216
+ {
217
+ tmpFetchOptions.headers = { 'X-Op-Id': tmpOp.OperationId };
218
+ }
219
+
220
+ fetch(tmpURL, tmpFetchOptions)
186
221
  .then((pResponse) => pResponse.json())
187
222
  .then((pData) =>
188
223
  {
189
224
  if (!pData || !pData.Success)
190
225
  {
226
+ if (tmpOp && tmpStatus)
227
+ {
228
+ tmpStatus.errorOperation(tmpOp.OperationId, { message: pData ? pData.Error : 'Unknown error' });
229
+ }
191
230
  tmpSelf._showError(pData ? pData.Error : 'Unknown error');
192
231
  return;
193
232
  }
194
233
 
234
+ if (tmpOp && tmpStatus)
235
+ {
236
+ tmpStatus.completeOperation(tmpOp.OperationId);
237
+ }
238
+ tmpSelf._activeOperationId = null;
239
+ tmpSelf._activeAbortController = null;
240
+
195
241
  tmpSelf._waveformData = pData;
196
242
  tmpSelf._peaks = pData.Peaks || [];
197
243
  tmpSelf._renderWaveformUI();
198
244
  })
199
245
  .catch((pError) =>
200
246
  {
247
+ if (pError && pError.name === 'AbortError')
248
+ {
249
+ return;
250
+ }
251
+ if (tmpOp && tmpStatus)
252
+ {
253
+ tmpStatus.errorOperation(tmpOp.OperationId, pError);
254
+ }
201
255
  tmpSelf._showError(pError.message);
202
256
  });
203
257
  }
204
258
 
259
+ /**
260
+ * Cancel any in-flight waveform fetch. Called on navigate-away and
261
+ * when launching a new waveform extraction.
262
+ */
263
+ _cancelActiveOperation()
264
+ {
265
+ if (this._activeAbortController)
266
+ {
267
+ try { this._activeAbortController.abort(); } catch (pErr) { /* ignore */ }
268
+ }
269
+ let tmpStatus = this.pict.providers['RetoldRemote-OperationStatus'];
270
+ if (this._activeOperationId && tmpStatus)
271
+ {
272
+ tmpStatus.cancelOperation(this._activeOperationId);
273
+ }
274
+ this._activeOperationId = null;
275
+ this._activeAbortController = null;
276
+ }
277
+
205
278
  /**
206
279
  * Render the waveform UI after data is loaded.
207
280
  */
@@ -981,6 +1054,9 @@ class RetoldRemoteAudioExplorerView extends libPictView
981
1054
  */
982
1055
  goBack()
983
1056
  {
1057
+ // Cancel any in-flight waveform extraction
1058
+ this._cancelActiveOperation();
1059
+
984
1060
  // Clean up resize observer
985
1061
  if (this._resizeObserver)
986
1062
  {
@@ -387,8 +387,18 @@ class RetoldRemoteCollectionsPanelView extends libPictView
387
387
  tmpManager.sortActiveCollection(null, tmpNewDir);
388
388
  };
389
389
 
390
+ let tmpExportBtn = document.createElement('button');
391
+ tmpExportBtn.className = 'retold-remote-collections-export-btn';
392
+ tmpExportBtn.textContent = '\u21e9 Export';
393
+ tmpExportBtn.title = 'Export collection to a folder';
394
+ tmpExportBtn.onclick = () =>
395
+ {
396
+ tmpSelf._showExportDialog(tmpCollection.GUID, tmpCollection.Name);
397
+ };
398
+
390
399
  tmpControls.appendChild(tmpSortSelect);
391
400
  tmpControls.appendChild(tmpDirBtn);
401
+ tmpControls.appendChild(tmpExportBtn);
392
402
  pRoot.appendChild(tmpControls);
393
403
  }
394
404
 
@@ -986,6 +996,209 @@ class RetoldRemoteCollectionsPanelView extends libPictView
986
996
  return '\uD83D\uDCC4';
987
997
  }
988
998
  }
999
+
1000
+ // -- Export Dialog -------------------------------------------------------
1001
+
1002
+ /**
1003
+ * Show the export dialog for a collection.
1004
+ *
1005
+ * @param {string} pGUID - Collection GUID
1006
+ * @param {string} pCollectionName - Collection name (for default folder)
1007
+ */
1008
+ _showExportDialog(pGUID, pCollectionName)
1009
+ {
1010
+ let tmpSelf = this;
1011
+
1012
+ // Build a default folder name from the collection name
1013
+ let tmpDefaultFolder = (pCollectionName || 'collection-export')
1014
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
1015
+ .replace(/\s+/g, '_')
1016
+ .substring(0, 80);
1017
+
1018
+ // Use the current browsed folder as a prefix if available
1019
+ let tmpRemote = this.pict.AppData.RetoldRemote;
1020
+ let tmpCurrentFolder = tmpRemote.CurrentBrowsedFolder || '';
1021
+ let tmpDefaultPath = tmpCurrentFolder
1022
+ ? tmpCurrentFolder.replace(/\/+$/, '') + '/' + tmpDefaultFolder
1023
+ : tmpDefaultFolder;
1024
+
1025
+ // Create a simple inline prompt in the collections detail area
1026
+ let tmpRoot = document.getElementById('RetoldRemote-Collections-Detail');
1027
+ if (!tmpRoot)
1028
+ {
1029
+ return;
1030
+ }
1031
+
1032
+ // Find or create the export dialog container
1033
+ let tmpExistingDialog = document.getElementById('RetoldRemote-ExportDialog');
1034
+ if (tmpExistingDialog)
1035
+ {
1036
+ tmpExistingDialog.parentElement.removeChild(tmpExistingDialog);
1037
+ }
1038
+
1039
+ let tmpDialog = document.createElement('div');
1040
+ tmpDialog.id = 'RetoldRemote-ExportDialog';
1041
+ tmpDialog.style.cssText = 'padding:10px;border-top:1px solid var(--retold-border);background:var(--retold-bg-secondary,#21252b);';
1042
+
1043
+ tmpDialog.innerHTML =
1044
+ '<div style="font-size:0.78rem;color:var(--retold-text);margin-bottom:6px;">Export to folder (within content root):</div>'
1045
+ + '<input type="text" id="RetoldRemote-ExportPath" value="' + tmpDefaultPath.replace(/"/g, '&quot;') + '" '
1046
+ + 'style="width:100%;box-sizing:border-box;background:var(--retold-bg-input,#1e1e1e);color:var(--retold-text);border:1px solid var(--retold-border);border-radius:4px;padding:4px 8px;font-size:0.78rem;font-family:inherit;margin-bottom:6px;">'
1047
+ + '<div style="display:flex;gap:6px;">'
1048
+ + '<button id="RetoldRemote-ExportConfirmBtn" style="flex:1;padding:4px 8px;font-size:0.75rem;">Export</button>'
1049
+ + '<button id="RetoldRemote-ExportCancelBtn" style="padding:4px 8px;font-size:0.75rem;">Cancel</button>'
1050
+ + '</div>'
1051
+ + '<div id="RetoldRemote-ExportStatus" style="font-size:0.72rem;color:var(--retold-text-dim);margin-top:6px;display:none;"></div>';
1052
+
1053
+ tmpRoot.appendChild(tmpDialog);
1054
+
1055
+ // Focus the path input
1056
+ let tmpPathInput = document.getElementById('RetoldRemote-ExportPath');
1057
+ if (tmpPathInput)
1058
+ {
1059
+ tmpPathInput.focus();
1060
+ tmpPathInput.select();
1061
+ }
1062
+
1063
+ // Cancel handler
1064
+ document.getElementById('RetoldRemote-ExportCancelBtn').onclick = () =>
1065
+ {
1066
+ tmpDialog.parentElement.removeChild(tmpDialog);
1067
+ };
1068
+
1069
+ // Export handler
1070
+ document.getElementById('RetoldRemote-ExportConfirmBtn').onclick = () =>
1071
+ {
1072
+ let tmpDestPath = tmpPathInput ? tmpPathInput.value.trim() : '';
1073
+ if (!tmpDestPath)
1074
+ {
1075
+ return;
1076
+ }
1077
+
1078
+ let tmpBtn = document.getElementById('RetoldRemote-ExportConfirmBtn');
1079
+ let tmpStatus = document.getElementById('RetoldRemote-ExportStatus');
1080
+ if (tmpBtn) tmpBtn.disabled = true;
1081
+ if (tmpBtn) tmpBtn.textContent = 'Exporting\u2026';
1082
+ if (tmpStatus) tmpStatus.style.display = '';
1083
+ if (tmpStatus) tmpStatus.textContent = 'Exporting\u2026';
1084
+
1085
+ // Start an operation entry so the status strip shows live progress
1086
+ // (individual item count, cancel button, etc.)
1087
+ let tmpOpStatus = tmpSelf.pict.providers['RetoldRemote-OperationStatus'];
1088
+ let tmpOp = tmpOpStatus ? tmpOpStatus.startOperation(
1089
+ {
1090
+ Label: 'Exporting collection' + (pCollectionName ? ': ' + pCollectionName : ''),
1091
+ Phase: 'Starting export…',
1092
+ Cancelable: true
1093
+ }) : null;
1094
+
1095
+ let tmpFetchOptions =
1096
+ {
1097
+ method: 'POST',
1098
+ headers: { 'Content-Type': 'application/json' },
1099
+ body: JSON.stringify({ DestinationPath: tmpDestPath })
1100
+ };
1101
+ if (tmpOp && tmpOp.AbortController)
1102
+ {
1103
+ tmpFetchOptions.signal = tmpOp.AbortController.signal;
1104
+ }
1105
+ if (tmpOp)
1106
+ {
1107
+ tmpFetchOptions.headers['X-Op-Id'] = tmpOp.OperationId;
1108
+ }
1109
+
1110
+ fetch('/api/collections/' + encodeURIComponent(pGUID) + '/export', tmpFetchOptions)
1111
+ .then((pResponse) => pResponse.json())
1112
+ .then((pResult) =>
1113
+ {
1114
+ if (pResult && pResult.Success)
1115
+ {
1116
+ if (tmpOp && tmpOpStatus)
1117
+ {
1118
+ tmpOpStatus.completeOperation(tmpOp.OperationId);
1119
+ }
1120
+
1121
+ let tmpMsg = 'Exported ' + pResult.ExportedCount + ' of ' + pResult.TotalItems + ' items';
1122
+ if (pResult.ErrorCount > 0)
1123
+ {
1124
+ tmpMsg += ' (' + pResult.ErrorCount + ' errors)';
1125
+ }
1126
+ if (pResult.Cancelled)
1127
+ {
1128
+ tmpMsg = 'Export cancelled after ' + pResult.ExportedCount + ' of ' + pResult.TotalItems + ' items';
1129
+ }
1130
+ tmpMsg += ' to ' + pResult.DestinationPath;
1131
+
1132
+ if (tmpStatus) tmpStatus.textContent = tmpMsg;
1133
+ if (tmpBtn) tmpBtn.textContent = pResult.Cancelled ? 'Close' : 'Done';
1134
+
1135
+ let tmpToast = tmpSelf.pict.providers['RetoldRemote-ToastNotification'];
1136
+ if (tmpToast)
1137
+ {
1138
+ tmpToast.showToast(tmpMsg);
1139
+ }
1140
+
1141
+ // Auto-dismiss after a moment (only on success)
1142
+ if (!pResult.Cancelled)
1143
+ {
1144
+ setTimeout(() =>
1145
+ {
1146
+ if (tmpDialog.parentElement)
1147
+ {
1148
+ tmpDialog.parentElement.removeChild(tmpDialog);
1149
+ }
1150
+ }, 3000);
1151
+ }
1152
+ }
1153
+ else
1154
+ {
1155
+ if (tmpOp && tmpOpStatus)
1156
+ {
1157
+ tmpOpStatus.errorOperation(tmpOp.OperationId, { message: (pResult && pResult.Error) || 'Export failed' });
1158
+ }
1159
+ let tmpErrMsg = (pResult && pResult.Error) || 'Export failed';
1160
+ if (tmpStatus) tmpStatus.textContent = tmpErrMsg;
1161
+ if (tmpStatus) tmpStatus.style.color = '#e06c75';
1162
+ if (tmpBtn) tmpBtn.textContent = 'Retry';
1163
+ if (tmpBtn) tmpBtn.disabled = false;
1164
+ }
1165
+ })
1166
+ .catch((pError) =>
1167
+ {
1168
+ if (pError && pError.name === 'AbortError')
1169
+ {
1170
+ if (tmpStatus) tmpStatus.textContent = 'Cancelled';
1171
+ if (tmpBtn) tmpBtn.textContent = 'Close';
1172
+ if (tmpBtn) tmpBtn.disabled = false;
1173
+ return;
1174
+ }
1175
+ if (tmpOp && tmpOpStatus)
1176
+ {
1177
+ tmpOpStatus.errorOperation(tmpOp.OperationId, pError);
1178
+ }
1179
+ if (tmpStatus) tmpStatus.textContent = 'Request failed: ' + pError.message;
1180
+ if (tmpStatus) tmpStatus.style.color = '#e06c75';
1181
+ if (tmpBtn) tmpBtn.textContent = 'Retry';
1182
+ if (tmpBtn) tmpBtn.disabled = false;
1183
+ });
1184
+ };
1185
+
1186
+ // Enter key in input triggers export
1187
+ if (tmpPathInput)
1188
+ {
1189
+ tmpPathInput.onkeydown = (pEvent) =>
1190
+ {
1191
+ if (pEvent.key === 'Enter')
1192
+ {
1193
+ document.getElementById('RetoldRemote-ExportConfirmBtn').click();
1194
+ }
1195
+ if (pEvent.key === 'Escape')
1196
+ {
1197
+ tmpDialog.parentElement.removeChild(tmpDialog);
1198
+ }
1199
+ };
1200
+ }
1201
+ }
989
1202
  }
990
1203
 
991
1204
  RetoldRemoteCollectionsPanelView.default_configuration = _ViewConfiguration;