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.
- package/css/retold-remote.css +343 -20
- package/docs/.nojekyll +0 -0
- package/docs/README.md +64 -12
- package/docs/_cover.md +6 -6
- package/docs/_sidebar.md +2 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/collections.md +30 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +62 -2
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +254 -0
- package/docs/retold-keyword-index.json +31216 -0
- package/docs/server-setup.md +122 -91
- package/docs/stack-launcher.md +218 -0
- package/docs/synology.md +585 -0
- package/docs/ultravisor-configuration.md +5 -5
- package/docs/ultravisor-integration.md +4 -2
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +22 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +460 -7
- package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
- package/source/providers/Pict-Provider-OperationStatus.js +597 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
- package/source/server/RetoldRemote-CollectionExportService.js +763 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +218 -3
- package/source/server/RetoldRemote-ImageService.js +221 -46
- package/source/server/RetoldRemote-MediaService.js +63 -4
- package/source/server/RetoldRemote-MetadataCache.js +25 -5
- package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
- package/source/server/RetoldRemote-SubimageService.js +680 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
- package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
- package/source/server/RetoldRemote-VideoFrameService.js +302 -9
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +1050 -0
- package/source/views/PictView-Remote-AudioExplorer.js +77 -1
- package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
- package/source/views/PictView-Remote-Gallery.js +365 -64
- package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +58 -0
- package/source/views/PictView-Remote-MediaViewer.js +100 -25
- package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/source/views/PictView-Remote-TopBar.js +1 -0
- package/source/views/PictView-Remote-VideoExplorer.js +77 -1
- package/web-application/css/docuserve.css +277 -23
- package/web-application/css/retold-remote.css +343 -20
- package/web-application/docs/README.md +64 -12
- package/web-application/docs/_cover.md +6 -6
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/_topbar.md +1 -1
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +62 -2
- package/web-application/docs/server-setup.md +122 -91
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/synology.md +585 -0
- package/web-application/docs/ultravisor-configuration.md +5 -5
- package/web-application/docs/ultravisor-integration.md +4 -2
- package/web-application/js/pict-docuserve.min.js +12 -12
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +6596 -1784
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +75 -23
- 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, '&')
|
|
417
|
+
.replace(/</g, '<')
|
|
418
|
+
.replace(/>/g, '>')
|
|
419
|
+
.replace(/"/g, '"')
|
|
420
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
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();
|