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
@@ -0,0 +1,597 @@
1
+ const libPictProvider = require('pict-provider');
2
+
3
+ /**
4
+ * Pict-Provider-OperationStatus
5
+ *
6
+ * Client-side singleton that:
7
+ * 1. Maintains a sticky-bottom status strip showing active long-running
8
+ * operations (DZI tile generation, video frame extraction, document
9
+ * conversion, collection export, etc.)
10
+ * 2. Connects to the retold-remote server's /ws/operations WebSocket
11
+ * endpoint and routes incoming progress events to the matching entry
12
+ * 3. Provides AbortController-based cancellation on navigate-away
13
+ * 4. Provides an explicit × button for user-initiated cancellation
14
+ *
15
+ * Usage pattern from an explorer or viewer:
16
+ *
17
+ * let tmpStatus = this.pict.providers['RetoldRemote-OperationStatus'];
18
+ *
19
+ * // Cancel any previous op for this view
20
+ * if (this._activeOpId)
21
+ * {
22
+ * tmpStatus.cancelOperation(this._activeOpId);
23
+ * }
24
+ *
25
+ * // Start a new one
26
+ * let tmpOp = tmpStatus.startOperation({
27
+ * Label: 'Generating DZI tiles',
28
+ * Cancelable: true
29
+ * });
30
+ * this._activeOpId = tmpOp.OperationId;
31
+ * this._activeAbortController = tmpOp.AbortController;
32
+ *
33
+ * fetch(url, {
34
+ * signal: tmpOp.AbortController.signal,
35
+ * headers: { 'X-Op-Id': tmpOp.OperationId }
36
+ * })
37
+ * .then(r => r.json())
38
+ * .then(data => {
39
+ * tmpStatus.completeOperation(tmpOp.OperationId);
40
+ * // ...use data
41
+ * })
42
+ * .catch(err => {
43
+ * if (err.name === 'AbortError') return;
44
+ * tmpStatus.errorOperation(tmpOp.OperationId, err);
45
+ * });
46
+ *
47
+ * // On navigate-away:
48
+ * if (this._activeAbortController)
49
+ * {
50
+ * this._activeAbortController.abort();
51
+ * tmpStatus.cancelOperation(this._activeOpId);
52
+ * }
53
+ */
54
+ const _DefaultProviderConfiguration =
55
+ {
56
+ ProviderIdentifier: 'RetoldRemote-OperationStatus',
57
+ AutoInitialize: true,
58
+ AutoSolveWithApp: false
59
+ };
60
+
61
+ // Visible op count cap — extras are summarized as "+N more"
62
+ const MAX_VISIBLE_OPS = 3;
63
+ // How long to keep a completed entry visible before fading out
64
+ const COMPLETE_FADEOUT_MS = 1200;
65
+ // Reconnect backoff ladder (ms)
66
+ const RECONNECT_BACKOFF_MS = [1000, 2000, 4000, 8000, 15000, 30000];
67
+
68
+ class OperationStatusProvider extends libPictProvider
69
+ {
70
+ constructor(pFable, pOptions, pServiceHash)
71
+ {
72
+ super(pFable, pOptions, pServiceHash);
73
+ this.serviceType = 'RetoldRemoteOperationStatusProvider';
74
+
75
+ // Map<opId, {
76
+ // Id, Label, Phase, Current, Total, Message,
77
+ // Cancelable, AbortController, Started, Status
78
+ // }>
79
+ // Status: 'active' | 'complete' | 'error' | 'cancelled'
80
+ this._operations = new Map();
81
+
82
+ // Monotonic counter so opIds are unique even within one millisecond
83
+ this._opCounter = 0;
84
+
85
+ // WebSocket state
86
+ this._ws = null;
87
+ this._wsConnected = false;
88
+ this._wsReconnectAttempt = 0;
89
+ this._wsReconnectTimer = null;
90
+ this._wsShouldRun = false;
91
+ }
92
+
93
+ /**
94
+ * Called by pict-application once the provider is registered.
95
+ * We defer DOM wiring until the anchor element is present.
96
+ */
97
+ onPictInitialize()
98
+ {
99
+ // Try to connect the WebSocket as soon as the DOM is ready
100
+ if (typeof document !== 'undefined')
101
+ {
102
+ if (document.readyState === 'loading')
103
+ {
104
+ let tmpSelf = this;
105
+ document.addEventListener('DOMContentLoaded', function ()
106
+ {
107
+ tmpSelf._connectWebSocket();
108
+ }, { once: true });
109
+ }
110
+ else
111
+ {
112
+ this._connectWebSocket();
113
+ }
114
+ }
115
+ }
116
+
117
+ // -----------------------------------------------------------------
118
+ // Public API
119
+ // -----------------------------------------------------------------
120
+
121
+ /**
122
+ * Begin tracking a new operation. Returns the assigned OperationId
123
+ * and an AbortController the caller can hand to fetch().
124
+ *
125
+ * @param {object} pOptions
126
+ * @param {string} [pOptions.Label] - Display label
127
+ * @param {string} [pOptions.Phase] - Initial phase text
128
+ * @param {boolean} [pOptions.Cancelable] - Show × button (default true)
129
+ * @returns {{ OperationId: string, AbortController: AbortController }}
130
+ */
131
+ startOperation(pOptions)
132
+ {
133
+ let tmpOptions = pOptions || {};
134
+ let tmpOpId = this._newOperationId();
135
+
136
+ let tmpAbortController = null;
137
+ if (typeof AbortController !== 'undefined')
138
+ {
139
+ try
140
+ {
141
+ tmpAbortController = new AbortController();
142
+ }
143
+ catch (pErr)
144
+ {
145
+ tmpAbortController = null;
146
+ }
147
+ }
148
+
149
+ let tmpOp =
150
+ {
151
+ Id: tmpOpId,
152
+ Label: tmpOptions.Label || 'Working…',
153
+ Phase: tmpOptions.Phase || '',
154
+ Current: 0,
155
+ Total: 0,
156
+ Message: tmpOptions.Phase || '',
157
+ Cancelable: (tmpOptions.Cancelable !== false),
158
+ AbortController: tmpAbortController,
159
+ Started: Date.now(),
160
+ Status: 'active'
161
+ };
162
+ this._operations.set(tmpOpId, tmpOp);
163
+ this._render();
164
+
165
+ return { OperationId: tmpOpId, AbortController: tmpAbortController };
166
+ }
167
+
168
+ /**
169
+ * Merge a partial update into an existing operation's state.
170
+ */
171
+ updateOperation(pOperationId, pPartial)
172
+ {
173
+ let tmpOp = this._operations.get(pOperationId);
174
+ if (!tmpOp)
175
+ {
176
+ return;
177
+ }
178
+ if (pPartial && typeof pPartial === 'object')
179
+ {
180
+ for (let tmpKey in pPartial)
181
+ {
182
+ if (pPartial.hasOwnProperty(tmpKey))
183
+ {
184
+ tmpOp[tmpKey] = pPartial[tmpKey];
185
+ }
186
+ }
187
+ }
188
+ this._render();
189
+ }
190
+
191
+ /**
192
+ * Mark an operation as complete. Auto-dismisses after a short delay.
193
+ */
194
+ completeOperation(pOperationId, pResult)
195
+ {
196
+ let tmpOp = this._operations.get(pOperationId);
197
+ if (!tmpOp)
198
+ {
199
+ return;
200
+ }
201
+ tmpOp.Status = 'complete';
202
+ tmpOp.Message = 'Done';
203
+ this._render();
204
+
205
+ let tmpSelf = this;
206
+ setTimeout(function ()
207
+ {
208
+ tmpSelf._operations.delete(pOperationId);
209
+ tmpSelf._render();
210
+ }, COMPLETE_FADEOUT_MS);
211
+ }
212
+
213
+ /**
214
+ * Mark an operation as failed. Stays visible until the user dismisses.
215
+ */
216
+ errorOperation(pOperationId, pError)
217
+ {
218
+ let tmpOp = this._operations.get(pOperationId);
219
+ if (!tmpOp)
220
+ {
221
+ return;
222
+ }
223
+ tmpOp.Status = 'error';
224
+ tmpOp.Message = pError && pError.message ? pError.message : String(pError || 'Error');
225
+ this._render();
226
+ }
227
+
228
+ /**
229
+ * Cancel an operation: abort the local fetch, send a cancel message
230
+ * over the WebSocket so the server can stop the corresponding service
231
+ * work, and remove the entry from the strip.
232
+ */
233
+ cancelOperation(pOperationId)
234
+ {
235
+ let tmpOp = this._operations.get(pOperationId);
236
+ if (!tmpOp)
237
+ {
238
+ return;
239
+ }
240
+
241
+ // Abort the local fetch (silent — caller's .catch filters AbortError)
242
+ if (tmpOp.AbortController && typeof tmpOp.AbortController.abort === 'function')
243
+ {
244
+ try
245
+ {
246
+ tmpOp.AbortController.abort();
247
+ }
248
+ catch (pErr)
249
+ {
250
+ // ignore
251
+ }
252
+ }
253
+
254
+ // Tell the server to stop the corresponding work
255
+ this._sendWs({ Type: 'cancel', OperationId: pOperationId });
256
+
257
+ tmpOp.Status = 'cancelled';
258
+ tmpOp.Message = 'Cancelled';
259
+ this._render();
260
+
261
+ // Remove after a brief visual acknowledgement
262
+ let tmpSelf = this;
263
+ setTimeout(function ()
264
+ {
265
+ tmpSelf._operations.delete(pOperationId);
266
+ tmpSelf._render();
267
+ }, 600);
268
+ }
269
+
270
+ /**
271
+ * Dismiss an entry without cancelling it (used for completed/errored
272
+ * entries after the user clicks × on the strip).
273
+ */
274
+ dismissOperation(pOperationId)
275
+ {
276
+ if (this._operations.delete(pOperationId))
277
+ {
278
+ this._render();
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Return true if a given opId is currently active (for use by
284
+ * WebSocket reconnect logic — only re-subscribe to ops we care about).
285
+ */
286
+ hasOperation(pOperationId)
287
+ {
288
+ return this._operations.has(pOperationId);
289
+ }
290
+
291
+ // -----------------------------------------------------------------
292
+ // Internals — DOM rendering
293
+ // -----------------------------------------------------------------
294
+
295
+ _newOperationId()
296
+ {
297
+ this._opCounter++;
298
+ return 'op-' + Date.now() + '-' + this._opCounter;
299
+ }
300
+
301
+ _render()
302
+ {
303
+ if (typeof document === 'undefined')
304
+ {
305
+ return;
306
+ }
307
+ let tmpContainer = document.getElementById('RetoldRemote-OperationStatus-Container');
308
+ if (!tmpContainer)
309
+ {
310
+ // Anchor not present yet (app still booting). Try again next tick.
311
+ return;
312
+ }
313
+
314
+ if (this._operations.size === 0)
315
+ {
316
+ tmpContainer.innerHTML = '';
317
+ tmpContainer.classList.remove('has-ops');
318
+ return;
319
+ }
320
+
321
+ tmpContainer.classList.add('has-ops');
322
+
323
+ // Convert to array, keep insertion order (Map preserves it)
324
+ let tmpAllOps = Array.from(this._operations.values());
325
+ let tmpVisible = tmpAllOps.slice(0, MAX_VISIBLE_OPS);
326
+ let tmpExtraCount = tmpAllOps.length - tmpVisible.length;
327
+
328
+ let tmpHTML = '<div class="retold-remote-operation-status-list">';
329
+ for (let i = 0; i < tmpVisible.length; i++)
330
+ {
331
+ tmpHTML += this._renderItem(tmpVisible[i]);
332
+ }
333
+ if (tmpExtraCount > 0)
334
+ {
335
+ tmpHTML += '<div class="retold-remote-operation-status-more">+' + tmpExtraCount + ' more\u2026</div>';
336
+ }
337
+ tmpHTML += '</div>';
338
+
339
+ tmpContainer.innerHTML = tmpHTML;
340
+ }
341
+
342
+ _renderItem(pOp)
343
+ {
344
+ let tmpStatusClass = '';
345
+ let tmpIconHTML = '';
346
+
347
+ if (pOp.Status === 'complete')
348
+ {
349
+ tmpStatusClass = ' is-complete';
350
+ tmpIconHTML = '<span class="retold-remote-operation-status-check">\u2714</span>';
351
+ }
352
+ else if (pOp.Status === 'error')
353
+ {
354
+ tmpStatusClass = ' is-error';
355
+ tmpIconHTML = '<span class="retold-remote-operation-status-error-icon">\u26a0</span>';
356
+ }
357
+ else if (pOp.Status === 'cancelled')
358
+ {
359
+ tmpStatusClass = ' is-cancelled';
360
+ tmpIconHTML = '<span class="retold-remote-operation-status-error-icon">\u00d7</span>';
361
+ }
362
+ else
363
+ {
364
+ tmpIconHTML = '<span class="retold-remote-operation-status-spinner"></span>';
365
+ }
366
+
367
+ // Progress bar vs indeterminate
368
+ let tmpBarHTML = '';
369
+ let tmpCountHTML = '';
370
+ if (pOp.Total && pOp.Total > 0)
371
+ {
372
+ let tmpPct = Math.min(100, Math.max(0, Math.round((pOp.Current / pOp.Total) * 100)));
373
+ tmpBarHTML = '<div class="retold-remote-operation-status-bar">'
374
+ + '<div class="retold-remote-operation-status-bar-fill" style="width:' + tmpPct + '%;"></div>'
375
+ + '</div>';
376
+ tmpCountHTML = '<span class="retold-remote-operation-status-count">' + pOp.Current + ' / ' + pOp.Total + '</span>';
377
+ }
378
+
379
+ let tmpLabel = this._escapeHtml(pOp.Label || '');
380
+ let tmpMessage = this._escapeHtml(pOp.Message || pOp.Phase || '');
381
+
382
+ let tmpCancelHTML = '';
383
+ if (pOp.Cancelable && pOp.Status === 'active')
384
+ {
385
+ tmpCancelHTML = '<button class="retold-remote-operation-status-cancel" '
386
+ + 'onclick="pict.providers[\'RetoldRemote-OperationStatus\'].cancelOperation(\'' + pOp.Id + '\')" '
387
+ + 'title="Cancel">\u00d7</button>';
388
+ }
389
+ else if (pOp.Status === 'error' || pOp.Status === 'cancelled')
390
+ {
391
+ // Dismiss button for completed errors
392
+ tmpCancelHTML = '<button class="retold-remote-operation-status-cancel" '
393
+ + 'onclick="pict.providers[\'RetoldRemote-OperationStatus\'].dismissOperation(\'' + pOp.Id + '\')" '
394
+ + 'title="Dismiss">\u00d7</button>';
395
+ }
396
+
397
+ return '<div class="retold-remote-operation-status-item' + tmpStatusClass + '" data-op-id="' + pOp.Id + '">'
398
+ + tmpIconHTML
399
+ + '<div class="retold-remote-operation-status-text">'
400
+ + '<div class="retold-remote-operation-status-label">' + tmpLabel + '</div>'
401
+ + '<div class="retold-remote-operation-status-phase">' + tmpMessage + '</div>'
402
+ + tmpBarHTML
403
+ + '</div>'
404
+ + tmpCountHTML
405
+ + tmpCancelHTML
406
+ + '</div>';
407
+ }
408
+
409
+ _escapeHtml(pStr)
410
+ {
411
+ if (pStr === null || pStr === undefined)
412
+ {
413
+ return '';
414
+ }
415
+ return String(pStr)
416
+ .replace(/&/g, '&amp;')
417
+ .replace(/</g, '&lt;')
418
+ .replace(/>/g, '&gt;')
419
+ .replace(/"/g, '&quot;')
420
+ .replace(/'/g, '&#39;');
421
+ }
422
+
423
+ // -----------------------------------------------------------------
424
+ // Internals — WebSocket client with reconnect
425
+ // -----------------------------------------------------------------
426
+
427
+ _connectWebSocket()
428
+ {
429
+ if (typeof WebSocket === 'undefined')
430
+ {
431
+ return;
432
+ }
433
+
434
+ this._wsShouldRun = true;
435
+
436
+ // Build the URL from the current page location so we work on any
437
+ // host / port the user hits (native dev or Docker).
438
+ let tmpProto = (typeof window !== 'undefined' && window.location && window.location.protocol === 'https:')
439
+ ? 'wss:'
440
+ : 'ws:';
441
+ let tmpHost = (typeof window !== 'undefined' && window.location && window.location.host)
442
+ ? window.location.host
443
+ : 'localhost';
444
+ let tmpUrl = tmpProto + '//' + tmpHost + '/ws/operations';
445
+
446
+ try
447
+ {
448
+ this._ws = new WebSocket(tmpUrl);
449
+ }
450
+ catch (pErr)
451
+ {
452
+ this._scheduleReconnect();
453
+ return;
454
+ }
455
+
456
+ let tmpSelf = this;
457
+ this._ws.addEventListener('open', function ()
458
+ {
459
+ tmpSelf._wsConnected = true;
460
+ tmpSelf._wsReconnectAttempt = 0;
461
+ });
462
+
463
+ this._ws.addEventListener('message', function (pEvent)
464
+ {
465
+ tmpSelf._onWsMessage(pEvent);
466
+ });
467
+
468
+ this._ws.addEventListener('close', function ()
469
+ {
470
+ tmpSelf._wsConnected = false;
471
+ tmpSelf._ws = null;
472
+ if (tmpSelf._wsShouldRun)
473
+ {
474
+ tmpSelf._scheduleReconnect();
475
+ }
476
+ });
477
+
478
+ this._ws.addEventListener('error', function ()
479
+ {
480
+ // Silent — the 'close' handler will attempt reconnect
481
+ });
482
+ }
483
+
484
+ _scheduleReconnect()
485
+ {
486
+ if (!this._wsShouldRun)
487
+ {
488
+ return;
489
+ }
490
+ if (this._wsReconnectTimer)
491
+ {
492
+ return;
493
+ }
494
+ let tmpDelay = RECONNECT_BACKOFF_MS[Math.min(this._wsReconnectAttempt, RECONNECT_BACKOFF_MS.length - 1)];
495
+ this._wsReconnectAttempt++;
496
+
497
+ let tmpSelf = this;
498
+ this._wsReconnectTimer = setTimeout(function ()
499
+ {
500
+ tmpSelf._wsReconnectTimer = null;
501
+ tmpSelf._connectWebSocket();
502
+ }, tmpDelay);
503
+ }
504
+
505
+ _sendWs(pMessage)
506
+ {
507
+ if (!this._ws || !this._wsConnected)
508
+ {
509
+ return;
510
+ }
511
+ try
512
+ {
513
+ this._ws.send(JSON.stringify(pMessage));
514
+ }
515
+ catch (pErr)
516
+ {
517
+ // ignore
518
+ }
519
+ }
520
+
521
+ _onWsMessage(pEvent)
522
+ {
523
+ let tmpData;
524
+ try
525
+ {
526
+ tmpData = JSON.parse(pEvent.data);
527
+ }
528
+ catch (pErr)
529
+ {
530
+ return;
531
+ }
532
+
533
+ if (!tmpData || typeof tmpData.Type !== 'string')
534
+ {
535
+ return;
536
+ }
537
+
538
+ switch (tmpData.Type)
539
+ {
540
+ case 'hello':
541
+ // Connection handshake — nothing to do
542
+ break;
543
+
544
+ case 'progress':
545
+ if (this._operations.has(tmpData.OperationId))
546
+ {
547
+ this.updateOperation(tmpData.OperationId,
548
+ {
549
+ Phase: tmpData.Phase,
550
+ Current: typeof tmpData.Current === 'number' ? tmpData.Current : undefined,
551
+ Total: typeof tmpData.Total === 'number' ? tmpData.Total : undefined,
552
+ Message: tmpData.Message,
553
+ Cancelable: (typeof tmpData.Cancelable === 'boolean') ? tmpData.Cancelable : undefined
554
+ });
555
+ }
556
+ break;
557
+
558
+ case 'complete':
559
+ if (this._operations.has(tmpData.OperationId))
560
+ {
561
+ this.completeOperation(tmpData.OperationId);
562
+ }
563
+ break;
564
+
565
+ case 'error':
566
+ if (this._operations.has(tmpData.OperationId))
567
+ {
568
+ this.errorOperation(tmpData.OperationId, { message: tmpData.Error });
569
+ }
570
+ break;
571
+
572
+ case 'cancelled':
573
+ if (this._operations.has(tmpData.OperationId))
574
+ {
575
+ let tmpOp = this._operations.get(tmpData.OperationId);
576
+ tmpOp.Status = 'cancelled';
577
+ tmpOp.Message = 'Cancelled';
578
+ this._render();
579
+ let tmpSelf = this;
580
+ setTimeout(function ()
581
+ {
582
+ tmpSelf._operations.delete(tmpData.OperationId);
583
+ tmpSelf._render();
584
+ }, 600);
585
+ }
586
+ break;
587
+
588
+ case 'pong':
589
+ // Heartbeat response — nothing to do
590
+ break;
591
+ }
592
+ }
593
+ }
594
+
595
+ OperationStatusProvider.default_configuration = _DefaultProviderConfiguration;
596
+
597
+ module.exports = OperationStatusProvider;
@@ -16,7 +16,21 @@ function handleImageExplorerKey(pGalleryNav, pEvent)
16
16
  {
17
17
  case 'Escape':
18
18
  pEvent.preventDefault();
19
- tmpIEX.goBack();
19
+ // Escape unwinds one layer at a time: first exit edit mode
20
+ // (if active), then exit new-region selection mode (if active),
21
+ // and only close the whole explorer if neither is active.
22
+ if (tmpIEX._editingRegionID)
23
+ {
24
+ tmpIEX._exitRegionEditMode();
25
+ }
26
+ else if (tmpIEX._selectionMode)
27
+ {
28
+ tmpIEX._exitSelectionMode();
29
+ }
30
+ else
31
+ {
32
+ tmpIEX.goBack();
33
+ }
20
34
  break;
21
35
  case '+':
22
36
  case '=':
@@ -77,6 +91,11 @@ function handleImageExplorerKey(pGalleryNav, pEvent)
77
91
  }
78
92
  }
79
93
  break;
94
+
95
+ case 's':
96
+ pEvent.preventDefault();
97
+ tmpIEX.toggleSelectionMode();
98
+ break;
80
99
  }
81
100
  }
82
101
 
@@ -166,6 +166,29 @@ function handleViewerKey(pGalleryNav, pEvent)
166
166
  pGalleryNav._cycleFitMode();
167
167
  break;
168
168
 
169
+ case 's':
170
+ pEvent.preventDefault();
171
+ {
172
+ let tmpMediaViewer = pGalleryNav.pict.views['RetoldRemote-MediaViewer'];
173
+ if (tmpMediaViewer)
174
+ {
175
+ let tmpViewerMediaType = tmpRemote.CurrentViewerMediaType;
176
+ if (tmpViewerMediaType === 'document')
177
+ {
178
+ // Toggle region selection for EPUB or PDF
179
+ if (typeof tmpMediaViewer.ebookToggleRegionSelect === 'function' && tmpMediaViewer._activeRendition)
180
+ {
181
+ tmpMediaViewer.ebookToggleRegionSelect();
182
+ }
183
+ else if (typeof tmpMediaViewer.pdfToggleRegionSelect === 'function' && tmpMediaViewer._pdfDocument)
184
+ {
185
+ tmpMediaViewer.pdfToggleRegionSelect();
186
+ }
187
+ }
188
+ }
189
+ }
190
+ break;
191
+
169
192
  case 'Enter':
170
193
  pEvent.preventDefault();
171
194
  pGalleryNav._streamWithVLC();