retold-remote 0.0.1 → 0.0.2

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 (33) hide show
  1. package/html/index.html +2 -0
  2. package/package.json +20 -14
  3. package/source/Pict-Application-RetoldRemote.js +46 -5
  4. package/source/cli/RetoldRemote-CLI-Run.js +0 -0
  5. package/source/cli/RetoldRemote-Server-Setup.js +790 -8
  6. package/source/cli/commands/RetoldRemote-Command-Serve.js +34 -1
  7. package/source/providers/Pict-Provider-GalleryFilterSort.js +61 -9
  8. package/source/providers/Pict-Provider-GalleryNavigation.js +517 -18
  9. package/source/providers/Pict-Provider-RetoldRemote.js +11 -2
  10. package/source/providers/Pict-Provider-RetoldRemoteIcons.js +1 -0
  11. package/source/server/RetoldRemote-ArchiveService.js +830 -0
  12. package/source/server/RetoldRemote-AudioWaveformService.js +673 -0
  13. package/source/server/RetoldRemote-EbookService.js +242 -0
  14. package/source/server/RetoldRemote-MediaService.js +1 -1
  15. package/source/server/RetoldRemote-ToolDetector.js +31 -1
  16. package/source/server/RetoldRemote-VideoFrameService.js +486 -0
  17. package/source/views/PictView-Remote-AudioExplorer.js +1213 -0
  18. package/source/views/PictView-Remote-Gallery.js +141 -2
  19. package/source/views/PictView-Remote-Layout.js +18 -27
  20. package/source/views/PictView-Remote-MediaViewer.js +638 -39
  21. package/source/views/PictView-Remote-SettingsPanel.js +23 -0
  22. package/source/views/PictView-Remote-TopBar.js +121 -0
  23. package/source/views/PictView-Remote-VideoExplorer.js +1229 -0
  24. package/web-application/index.html +2 -0
  25. package/web-application/js/epub.min.js +1 -0
  26. package/web-application/retold-remote.js +7030 -1244
  27. package/web-application/retold-remote.js.map +1 -1
  28. package/web-application/retold-remote.min.js +13 -44
  29. package/web-application/retold-remote.min.js.map +1 -1
  30. package/web-application/retold-remote.compatible.js +0 -5764
  31. package/web-application/retold-remote.compatible.js.map +0 -1
  32. package/web-application/retold-remote.compatible.min.js +0 -120
  33. package/web-application/retold-remote.compatible.min.js.map +0 -1
@@ -0,0 +1,1213 @@
1
+ const libPictView = require('pict-view');
2
+
3
+ const _ViewConfiguration =
4
+ {
5
+ ViewIdentifier: "RetoldRemote-AudioExplorer",
6
+ DefaultRenderable: "RetoldRemote-AudioExplorer",
7
+ DefaultDestinationAddress: "#RetoldRemote-Viewer-Container",
8
+ AutoRender: false,
9
+
10
+ CSS: /*css*/`
11
+ .retold-remote-aex
12
+ {
13
+ display: flex;
14
+ flex-direction: column;
15
+ height: 100%;
16
+ }
17
+ .retold-remote-aex-header
18
+ {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 12px;
22
+ padding: 8px 16px;
23
+ background: var(--retold-bg-secondary);
24
+ border-bottom: 1px solid var(--retold-border);
25
+ flex-shrink: 0;
26
+ z-index: 5;
27
+ }
28
+ .retold-remote-aex-nav-btn
29
+ {
30
+ padding: 4px 10px;
31
+ border: 1px solid var(--retold-border);
32
+ border-radius: 3px;
33
+ background: transparent;
34
+ color: var(--retold-text-muted);
35
+ font-size: 0.8rem;
36
+ cursor: pointer;
37
+ transition: color 0.15s, border-color 0.15s;
38
+ font-family: inherit;
39
+ }
40
+ .retold-remote-aex-nav-btn:hover
41
+ {
42
+ color: var(--retold-text-primary);
43
+ border-color: var(--retold-accent);
44
+ }
45
+ .retold-remote-aex-title
46
+ {
47
+ flex: 1;
48
+ font-size: 0.82rem;
49
+ color: var(--retold-text-secondary);
50
+ overflow: hidden;
51
+ text-overflow: ellipsis;
52
+ white-space: nowrap;
53
+ text-align: center;
54
+ }
55
+ .retold-remote-aex-info
56
+ {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 16px;
60
+ padding: 8px 16px;
61
+ background: var(--retold-bg-tertiary);
62
+ border-bottom: 1px solid var(--retold-border);
63
+ flex-shrink: 0;
64
+ font-size: 0.75rem;
65
+ color: var(--retold-text-dim);
66
+ flex-wrap: wrap;
67
+ }
68
+ .retold-remote-aex-info-item
69
+ {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ gap: 4px;
73
+ }
74
+ .retold-remote-aex-info-label
75
+ {
76
+ color: var(--retold-text-muted);
77
+ }
78
+ .retold-remote-aex-info-value
79
+ {
80
+ color: var(--retold-text-secondary);
81
+ }
82
+ .retold-remote-aex-controls
83
+ {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 12px;
87
+ padding: 8px 16px;
88
+ background: var(--retold-bg-secondary);
89
+ border-bottom: 1px solid var(--retold-border);
90
+ flex-shrink: 0;
91
+ flex-wrap: wrap;
92
+ }
93
+ .retold-remote-aex-controls label
94
+ {
95
+ font-size: 0.75rem;
96
+ color: var(--retold-text-muted);
97
+ }
98
+ .retold-remote-aex-controls select,
99
+ .retold-remote-aex-controls input
100
+ {
101
+ font-size: 0.75rem;
102
+ background: var(--retold-bg-tertiary);
103
+ color: var(--retold-text-primary);
104
+ border: 1px solid var(--retold-border);
105
+ border-radius: 3px;
106
+ padding: 2px 6px;
107
+ font-family: inherit;
108
+ }
109
+ .retold-remote-aex-btn
110
+ {
111
+ padding: 3px 12px;
112
+ border: 1px solid var(--retold-accent);
113
+ border-radius: 3px;
114
+ background: transparent;
115
+ color: var(--retold-accent);
116
+ font-size: 0.75rem;
117
+ cursor: pointer;
118
+ transition: background 0.15s, color 0.15s;
119
+ font-family: inherit;
120
+ }
121
+ .retold-remote-aex-btn:hover
122
+ {
123
+ background: var(--retold-accent);
124
+ color: var(--retold-bg-primary);
125
+ }
126
+ .retold-remote-aex-btn:disabled
127
+ {
128
+ opacity: 0.4;
129
+ cursor: not-allowed;
130
+ }
131
+ .retold-remote-aex-btn:disabled:hover
132
+ {
133
+ background: transparent;
134
+ color: var(--retold-accent);
135
+ }
136
+ .retold-remote-aex-body
137
+ {
138
+ flex: 1;
139
+ display: flex;
140
+ flex-direction: column;
141
+ overflow: hidden;
142
+ position: relative;
143
+ }
144
+ .retold-remote-aex-loading
145
+ {
146
+ display: flex;
147
+ flex-direction: column;
148
+ align-items: center;
149
+ justify-content: center;
150
+ height: 100%;
151
+ color: var(--retold-text-dim);
152
+ font-size: 0.9rem;
153
+ }
154
+ .retold-remote-aex-loading-spinner
155
+ {
156
+ width: 32px;
157
+ height: 32px;
158
+ border: 3px solid var(--retold-border);
159
+ border-top-color: var(--retold-accent);
160
+ border-radius: 50%;
161
+ animation: retold-aex-spin 0.8s linear infinite;
162
+ margin-bottom: 16px;
163
+ }
164
+ @keyframes retold-aex-spin
165
+ {
166
+ to { transform: rotate(360deg); }
167
+ }
168
+ .retold-remote-aex-canvas-wrap
169
+ {
170
+ flex: 1;
171
+ position: relative;
172
+ min-height: 150px;
173
+ cursor: crosshair;
174
+ }
175
+ .retold-remote-aex-canvas-wrap canvas
176
+ {
177
+ width: 100%;
178
+ height: 100%;
179
+ display: block;
180
+ }
181
+ .retold-remote-aex-overview-wrap
182
+ {
183
+ height: 48px;
184
+ position: relative;
185
+ background: var(--retold-bg-tertiary);
186
+ border-top: 1px solid var(--retold-border);
187
+ flex-shrink: 0;
188
+ cursor: pointer;
189
+ }
190
+ .retold-remote-aex-overview-wrap canvas
191
+ {
192
+ width: 100%;
193
+ height: 100%;
194
+ display: block;
195
+ }
196
+ .retold-remote-aex-overview-viewport
197
+ {
198
+ position: absolute;
199
+ top: 0;
200
+ height: 100%;
201
+ background: rgba(255, 255, 255, 0.08);
202
+ border-left: 2px solid var(--retold-accent);
203
+ border-right: 2px solid var(--retold-accent);
204
+ pointer-events: none;
205
+ }
206
+ /* Time display bar */
207
+ .retold-remote-aex-time-bar
208
+ {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 12px;
212
+ padding: 6px 16px;
213
+ background: var(--retold-bg-secondary);
214
+ border-top: 1px solid var(--retold-border);
215
+ flex-shrink: 0;
216
+ font-size: 0.75rem;
217
+ font-family: var(--retold-font-mono, monospace);
218
+ }
219
+ .retold-remote-aex-time-label
220
+ {
221
+ color: var(--retold-text-dim);
222
+ }
223
+ .retold-remote-aex-time-value
224
+ {
225
+ color: var(--retold-text-secondary);
226
+ }
227
+ .retold-remote-aex-time-selection
228
+ {
229
+ color: var(--retold-accent);
230
+ }
231
+ /* Playback bar */
232
+ .retold-remote-aex-playback
233
+ {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 10px;
237
+ padding: 8px 16px;
238
+ background: var(--retold-bg-secondary);
239
+ border-top: 1px solid var(--retold-border);
240
+ flex-shrink: 0;
241
+ }
242
+ .retold-remote-aex-playback audio
243
+ {
244
+ flex: 1;
245
+ height: 32px;
246
+ max-width: 400px;
247
+ }
248
+ .retold-remote-aex-playback-label
249
+ {
250
+ font-size: 0.72rem;
251
+ color: var(--retold-text-dim);
252
+ }
253
+ /* Error state */
254
+ .retold-remote-aex-error
255
+ {
256
+ display: flex;
257
+ flex-direction: column;
258
+ align-items: center;
259
+ justify-content: center;
260
+ height: 100%;
261
+ color: var(--retold-text-dim);
262
+ font-size: 0.85rem;
263
+ text-align: center;
264
+ padding: 40px;
265
+ }
266
+ .retold-remote-aex-error-message
267
+ {
268
+ color: #e06c75;
269
+ margin-bottom: 16px;
270
+ }
271
+ `
272
+ };
273
+
274
+ class RetoldRemoteAudioExplorerView extends libPictView
275
+ {
276
+ constructor(pFable, pOptions, pServiceHash)
277
+ {
278
+ super(pFable, pOptions, pServiceHash);
279
+
280
+ this._currentPath = '';
281
+ this._waveformData = null;
282
+ this._peaks = [];
283
+
284
+ // View state
285
+ this._viewStart = 0; // Start of visible range (0..1)
286
+ this._viewEnd = 1; // End of visible range (0..1)
287
+ this._minZoom = 0.005; // Minimum visible range (0.5% of total)
288
+
289
+ // Selection state
290
+ this._selectionStart = -1; // Selection start (0..1), -1 = none
291
+ this._selectionEnd = -1;
292
+ this._isDragging = false;
293
+ this._dragStart = -1;
294
+
295
+ // Cursor position
296
+ this._cursorX = -1;
297
+
298
+ // Playback
299
+ this._segmentURL = null;
300
+
301
+ // Canvas refs
302
+ this._mainCanvas = null;
303
+ this._overviewCanvas = null;
304
+ this._resizeObserver = null;
305
+ }
306
+
307
+ /**
308
+ * Show the audio explorer for a given audio file.
309
+ *
310
+ * @param {string} pFilePath - Relative file path
311
+ */
312
+ showExplorer(pFilePath)
313
+ {
314
+ let tmpRemote = this.pict.AppData.RetoldRemote;
315
+ tmpRemote.ActiveMode = 'audio-explorer';
316
+ this._currentPath = pFilePath;
317
+ this._waveformData = null;
318
+ this._peaks = [];
319
+ this._viewStart = 0;
320
+ this._viewEnd = 1;
321
+ this._selectionStart = -1;
322
+ this._selectionEnd = -1;
323
+ this._segmentURL = null;
324
+
325
+ // Update the hash
326
+ let tmpFragProvider = this.pict.providers['RetoldRemote-Provider'];
327
+ let tmpFragId = tmpFragProvider ? tmpFragProvider.getFragmentIdentifier(pFilePath) : pFilePath;
328
+ window.location.hash = '#/explore-audio/' + tmpFragId;
329
+
330
+ // Show viewer container, hide gallery
331
+ let tmpGalleryContainer = document.getElementById('RetoldRemote-Gallery-Container');
332
+ let tmpViewerContainer = document.getElementById('RetoldRemote-Viewer-Container');
333
+
334
+ if (tmpGalleryContainer) tmpGalleryContainer.style.display = 'none';
335
+ if (tmpViewerContainer) tmpViewerContainer.style.display = 'block';
336
+
337
+ let tmpFileName = pFilePath.replace(/^.*\//, '');
338
+
339
+ // Build initial UI
340
+ let tmpHTML = '<div class="retold-remote-aex">';
341
+
342
+ // Header
343
+ tmpHTML += '<div class="retold-remote-aex-header">';
344
+ tmpHTML += '<button class="retold-remote-aex-nav-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].goBack()" title="Back to audio (Esc)">&larr; Back</button>';
345
+ tmpHTML += '<div class="retold-remote-aex-title">Audio Explorer &mdash; ' + this._escapeHTML(tmpFileName) + '</div>';
346
+ tmpHTML += '</div>';
347
+
348
+ // Info bar (populated after waveform loads)
349
+ tmpHTML += '<div class="retold-remote-aex-info" id="RetoldRemote-AEX-Info" style="display:none;"></div>';
350
+
351
+ // Controls bar
352
+ tmpHTML += '<div class="retold-remote-aex-controls" id="RetoldRemote-AEX-Controls" style="display:none;">';
353
+ tmpHTML += '<button class="retold-remote-aex-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].zoomIn()" title="Zoom In (+)">+ Zoom In</button>';
354
+ tmpHTML += '<button class="retold-remote-aex-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].zoomOut()" title="Zoom Out (-)">- Zoom Out</button>';
355
+ tmpHTML += '<button class="retold-remote-aex-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].zoomToFit()" title="Zoom to Fit (0)">Fit All</button>';
356
+ tmpHTML += '<button class="retold-remote-aex-btn" id="RetoldRemote-AEX-ZoomSelBtn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].zoomToSelection()" title="Zoom to Selection (Z)" disabled>Zoom to Selection</button>';
357
+ tmpHTML += '<button class="retold-remote-aex-btn" id="RetoldRemote-AEX-PlaySelBtn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].playSelection()" title="Play Selection (Space)" disabled>&#9654; Play Selection</button>';
358
+ tmpHTML += '<button class="retold-remote-aex-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].clearSelection()" title="Clear Selection (Esc)">Clear Selection</button>';
359
+ tmpHTML += '</div>';
360
+
361
+ // Body (loading initially)
362
+ tmpHTML += '<div class="retold-remote-aex-body" id="RetoldRemote-AEX-Body">';
363
+ tmpHTML += '<div class="retold-remote-aex-loading">';
364
+ tmpHTML += '<div class="retold-remote-aex-loading-spinner"></div>';
365
+ tmpHTML += 'Analyzing audio waveform...';
366
+ tmpHTML += '</div>';
367
+ tmpHTML += '</div>';
368
+
369
+ // Time display bar
370
+ tmpHTML += '<div class="retold-remote-aex-time-bar" id="RetoldRemote-AEX-TimeBar" style="display:none;">';
371
+ tmpHTML += '<span class="retold-remote-aex-time-label">View:</span>';
372
+ tmpHTML += '<span class="retold-remote-aex-time-value" id="RetoldRemote-AEX-ViewRange">--</span>';
373
+ tmpHTML += '<span class="retold-remote-aex-time-label" style="margin-left: 12px;">Selection:</span>';
374
+ tmpHTML += '<span class="retold-remote-aex-time-selection" id="RetoldRemote-AEX-SelectionRange">None</span>';
375
+ tmpHTML += '<span class="retold-remote-aex-time-label" style="margin-left: 12px;">Cursor:</span>';
376
+ tmpHTML += '<span class="retold-remote-aex-time-value" id="RetoldRemote-AEX-CursorTime">--</span>';
377
+ tmpHTML += '</div>';
378
+
379
+ // Playback bar
380
+ tmpHTML += '<div class="retold-remote-aex-playback" id="RetoldRemote-AEX-Playback" style="display:none;">';
381
+ tmpHTML += '<span class="retold-remote-aex-playback-label">Segment:</span>';
382
+ tmpHTML += '<audio controls id="RetoldRemote-AEX-Audio"></audio>';
383
+ tmpHTML += '</div>';
384
+
385
+ tmpHTML += '</div>';
386
+
387
+ if (tmpViewerContainer)
388
+ {
389
+ tmpViewerContainer.innerHTML = tmpHTML;
390
+ }
391
+
392
+ // Update topbar
393
+ let tmpTopBar = this.pict.views['ContentEditor-TopBar'];
394
+ if (tmpTopBar)
395
+ {
396
+ tmpTopBar.updateInfo();
397
+ }
398
+
399
+ // Fetch waveform
400
+ this._fetchWaveform(pFilePath);
401
+ }
402
+
403
+ /**
404
+ * Fetch waveform data from the server.
405
+ *
406
+ * @param {string} pFilePath - Relative file path
407
+ */
408
+ _fetchWaveform(pFilePath)
409
+ {
410
+ let tmpSelf = this;
411
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
412
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(pFilePath) : encodeURIComponent(pFilePath);
413
+
414
+ let tmpURL = '/api/media/audio-waveform?path=' + tmpPathParam + '&peaks=2000';
415
+
416
+ fetch(tmpURL)
417
+ .then((pResponse) => pResponse.json())
418
+ .then((pData) =>
419
+ {
420
+ if (!pData || !pData.Success)
421
+ {
422
+ tmpSelf._showError(pData ? pData.Error : 'Unknown error');
423
+ return;
424
+ }
425
+
426
+ tmpSelf._waveformData = pData;
427
+ tmpSelf._peaks = pData.Peaks || [];
428
+ tmpSelf._renderWaveformUI();
429
+ })
430
+ .catch((pError) =>
431
+ {
432
+ tmpSelf._showError(pError.message);
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Render the waveform UI after data is loaded.
438
+ */
439
+ _renderWaveformUI()
440
+ {
441
+ let tmpData = this._waveformData;
442
+ if (!tmpData)
443
+ {
444
+ return;
445
+ }
446
+
447
+ // Populate info bar
448
+ let tmpInfoBar = document.getElementById('RetoldRemote-AEX-Info');
449
+ if (tmpInfoBar)
450
+ {
451
+ let tmpInfoHTML = '';
452
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Duration</span> <span class="retold-remote-aex-info-value">' + this._escapeHTML(tmpData.DurationFormatted) + '</span></span>';
453
+ if (tmpData.SampleRate)
454
+ {
455
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Sample Rate</span> <span class="retold-remote-aex-info-value">' + (tmpData.SampleRate / 1000).toFixed(1) + ' kHz</span></span>';
456
+ }
457
+ if (tmpData.Channels)
458
+ {
459
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Channels</span> <span class="retold-remote-aex-info-value">' + tmpData.Channels + (tmpData.ChannelLayout ? ' (' + this._escapeHTML(tmpData.ChannelLayout) + ')' : '') + '</span></span>';
460
+ }
461
+ if (tmpData.Codec)
462
+ {
463
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Codec</span> <span class="retold-remote-aex-info-value">' + this._escapeHTML(tmpData.Codec) + '</span></span>';
464
+ }
465
+ if (tmpData.Bitrate)
466
+ {
467
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Bitrate</span> <span class="retold-remote-aex-info-value">' + Math.round(tmpData.Bitrate / 1000) + ' kbps</span></span>';
468
+ }
469
+ if (tmpData.FileSize)
470
+ {
471
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Size</span> <span class="retold-remote-aex-info-value">' + this._formatFileSize(tmpData.FileSize) + '</span></span>';
472
+ }
473
+ tmpInfoHTML += '<span class="retold-remote-aex-info-item"><span class="retold-remote-aex-info-label">Peaks</span> <span class="retold-remote-aex-info-value">' + tmpData.PeakCount + ' (' + this._escapeHTML(tmpData.Method) + ')</span></span>';
474
+
475
+ tmpInfoBar.innerHTML = tmpInfoHTML;
476
+ tmpInfoBar.style.display = '';
477
+ }
478
+
479
+ // Show controls
480
+ let tmpControlsBar = document.getElementById('RetoldRemote-AEX-Controls');
481
+ if (tmpControlsBar)
482
+ {
483
+ tmpControlsBar.style.display = '';
484
+ }
485
+
486
+ // Show time bar
487
+ let tmpTimeBar = document.getElementById('RetoldRemote-AEX-TimeBar');
488
+ if (tmpTimeBar)
489
+ {
490
+ tmpTimeBar.style.display = '';
491
+ }
492
+
493
+ // Build the canvas-based waveform body
494
+ let tmpBody = document.getElementById('RetoldRemote-AEX-Body');
495
+ if (tmpBody)
496
+ {
497
+ let tmpBodyHTML = '';
498
+
499
+ // Main waveform canvas
500
+ tmpBodyHTML += '<div class="retold-remote-aex-canvas-wrap" id="RetoldRemote-AEX-CanvasWrap">';
501
+ tmpBodyHTML += '<canvas id="RetoldRemote-AEX-MainCanvas"></canvas>';
502
+ tmpBodyHTML += '</div>';
503
+
504
+ // Overview canvas
505
+ tmpBodyHTML += '<div class="retold-remote-aex-overview-wrap" id="RetoldRemote-AEX-OverviewWrap">';
506
+ tmpBodyHTML += '<canvas id="RetoldRemote-AEX-OverviewCanvas"></canvas>';
507
+ tmpBodyHTML += '<div class="retold-remote-aex-overview-viewport" id="RetoldRemote-AEX-OverviewViewport"></div>';
508
+ tmpBodyHTML += '</div>';
509
+
510
+ tmpBody.innerHTML = tmpBodyHTML;
511
+ }
512
+
513
+ // Set up canvases
514
+ this._mainCanvas = document.getElementById('RetoldRemote-AEX-MainCanvas');
515
+ this._overviewCanvas = document.getElementById('RetoldRemote-AEX-OverviewCanvas');
516
+
517
+ // Bind interactions
518
+ this._bindCanvasEvents();
519
+
520
+ // Initial draw
521
+ this._resizeCanvases();
522
+ this._drawAll();
523
+ this._updateTimeDisplay();
524
+
525
+ // Set up resize observer
526
+ let tmpSelf = this;
527
+ if (typeof ResizeObserver !== 'undefined')
528
+ {
529
+ this._resizeObserver = new ResizeObserver(() =>
530
+ {
531
+ tmpSelf._resizeCanvases();
532
+ tmpSelf._drawAll();
533
+ });
534
+ let tmpWrap = document.getElementById('RetoldRemote-AEX-CanvasWrap');
535
+ if (tmpWrap)
536
+ {
537
+ this._resizeObserver.observe(tmpWrap);
538
+ }
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Resize canvases to match their container's pixel dimensions.
544
+ */
545
+ _resizeCanvases()
546
+ {
547
+ if (this._mainCanvas)
548
+ {
549
+ let tmpWrap = this._mainCanvas.parentElement;
550
+ if (tmpWrap)
551
+ {
552
+ let tmpDPR = window.devicePixelRatio || 1;
553
+ this._mainCanvas.width = tmpWrap.clientWidth * tmpDPR;
554
+ this._mainCanvas.height = tmpWrap.clientHeight * tmpDPR;
555
+ }
556
+ }
557
+
558
+ if (this._overviewCanvas)
559
+ {
560
+ let tmpWrap = this._overviewCanvas.parentElement;
561
+ if (tmpWrap)
562
+ {
563
+ let tmpDPR = window.devicePixelRatio || 1;
564
+ this._overviewCanvas.width = tmpWrap.clientWidth * tmpDPR;
565
+ this._overviewCanvas.height = tmpWrap.clientHeight * tmpDPR;
566
+ }
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Draw both main and overview canvases.
572
+ */
573
+ _drawAll()
574
+ {
575
+ this._drawMainWaveform();
576
+ this._drawOverviewWaveform();
577
+ this._updateOverviewViewport();
578
+ }
579
+
580
+ /**
581
+ * Draw the main (zoomed) waveform canvas.
582
+ */
583
+ _drawMainWaveform()
584
+ {
585
+ if (!this._mainCanvas || this._peaks.length === 0)
586
+ {
587
+ return;
588
+ }
589
+
590
+ let tmpCtx = this._mainCanvas.getContext('2d');
591
+ let tmpW = this._mainCanvas.width;
592
+ let tmpH = this._mainCanvas.height;
593
+ let tmpDPR = window.devicePixelRatio || 1;
594
+
595
+ tmpCtx.clearRect(0, 0, tmpW, tmpH);
596
+
597
+ // Background
598
+ let tmpBgColor = getComputedStyle(document.documentElement).getPropertyValue('--retold-bg-primary').trim() || '#1e1e2e';
599
+ tmpCtx.fillStyle = tmpBgColor;
600
+ tmpCtx.fillRect(0, 0, tmpW, tmpH);
601
+
602
+ let tmpPeaks = this._peaks;
603
+ let tmpTotalPeaks = tmpPeaks.length;
604
+ let tmpStartIdx = Math.floor(this._viewStart * tmpTotalPeaks);
605
+ let tmpEndIdx = Math.ceil(this._viewEnd * tmpTotalPeaks);
606
+ let tmpVisiblePeaks = tmpEndIdx - tmpStartIdx;
607
+
608
+ if (tmpVisiblePeaks <= 0)
609
+ {
610
+ return;
611
+ }
612
+
613
+ let tmpMidY = tmpH / 2;
614
+
615
+ // Draw selection background
616
+ if (this._selectionStart >= 0 && this._selectionEnd >= 0)
617
+ {
618
+ let tmpSelStartNorm = Math.min(this._selectionStart, this._selectionEnd);
619
+ let tmpSelEndNorm = Math.max(this._selectionStart, this._selectionEnd);
620
+
621
+ // Convert selection (0..1) to pixel coordinates within the view
622
+ let tmpViewRange = this._viewEnd - this._viewStart;
623
+ let tmpSelStartPx = ((tmpSelStartNorm - this._viewStart) / tmpViewRange) * tmpW;
624
+ let tmpSelEndPx = ((tmpSelEndNorm - this._viewStart) / tmpViewRange) * tmpW;
625
+
626
+ tmpSelStartPx = Math.max(0, tmpSelStartPx);
627
+ tmpSelEndPx = Math.min(tmpW, tmpSelEndPx);
628
+
629
+ if (tmpSelEndPx > tmpSelStartPx)
630
+ {
631
+ let tmpAccent = getComputedStyle(document.documentElement).getPropertyValue('--retold-accent').trim() || '#89b4fa';
632
+ tmpCtx.fillStyle = tmpAccent + '22';
633
+ tmpCtx.fillRect(tmpSelStartPx, 0, tmpSelEndPx - tmpSelStartPx, tmpH);
634
+
635
+ // Selection edges
636
+ tmpCtx.strokeStyle = tmpAccent;
637
+ tmpCtx.lineWidth = 2 * tmpDPR;
638
+ tmpCtx.beginPath();
639
+ tmpCtx.moveTo(tmpSelStartPx, 0);
640
+ tmpCtx.lineTo(tmpSelStartPx, tmpH);
641
+ tmpCtx.stroke();
642
+ tmpCtx.beginPath();
643
+ tmpCtx.moveTo(tmpSelEndPx, 0);
644
+ tmpCtx.lineTo(tmpSelEndPx, tmpH);
645
+ tmpCtx.stroke();
646
+ }
647
+ }
648
+
649
+ // Draw center line
650
+ let tmpDimColor = getComputedStyle(document.documentElement).getPropertyValue('--retold-text-dim').trim() || '#585b70';
651
+ tmpCtx.strokeStyle = tmpDimColor;
652
+ tmpCtx.lineWidth = 1;
653
+ tmpCtx.setLineDash([4, 4]);
654
+ tmpCtx.beginPath();
655
+ tmpCtx.moveTo(0, tmpMidY);
656
+ tmpCtx.lineTo(tmpW, tmpMidY);
657
+ tmpCtx.stroke();
658
+ tmpCtx.setLineDash([]);
659
+
660
+ // Draw waveform
661
+ let tmpAccentColor = getComputedStyle(document.documentElement).getPropertyValue('--retold-accent').trim() || '#89b4fa';
662
+ let tmpSecondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--retold-text-secondary').trim() || '#cdd6f4';
663
+
664
+ // For each pixel column, find the min/max across the peaks that map to it
665
+ for (let x = 0; x < tmpW; x++)
666
+ {
667
+ let tmpPeakStartF = tmpStartIdx + (x / tmpW) * tmpVisiblePeaks;
668
+ let tmpPeakEndF = tmpStartIdx + ((x + 1) / tmpW) * tmpVisiblePeaks;
669
+ let tmpPStart = Math.floor(tmpPeakStartF);
670
+ let tmpPEnd = Math.ceil(tmpPeakEndF);
671
+ tmpPStart = Math.max(0, Math.min(tmpPStart, tmpTotalPeaks - 1));
672
+ tmpPEnd = Math.max(tmpPStart + 1, Math.min(tmpPEnd, tmpTotalPeaks));
673
+
674
+ let tmpMin = 0;
675
+ let tmpMax = 0;
676
+ for (let p = tmpPStart; p < tmpPEnd; p++)
677
+ {
678
+ if (tmpPeaks[p].Min < tmpMin) tmpMin = tmpPeaks[p].Min;
679
+ if (tmpPeaks[p].Max > tmpMax) tmpMax = tmpPeaks[p].Max;
680
+ }
681
+
682
+ let tmpTopY = tmpMidY - (tmpMax * tmpMidY * 0.9);
683
+ let tmpBottomY = tmpMidY - (tmpMin * tmpMidY * 0.9);
684
+ let tmpBarHeight = Math.max(1, tmpBottomY - tmpTopY);
685
+
686
+ // Check if this pixel column is in the selection
687
+ let tmpNormPos = this._viewStart + (x / tmpW) * (this._viewEnd - this._viewStart);
688
+ let tmpInSelection = false;
689
+ if (this._selectionStart >= 0 && this._selectionEnd >= 0)
690
+ {
691
+ let tmpSelMin = Math.min(this._selectionStart, this._selectionEnd);
692
+ let tmpSelMax = Math.max(this._selectionStart, this._selectionEnd);
693
+ tmpInSelection = (tmpNormPos >= tmpSelMin && tmpNormPos <= tmpSelMax);
694
+ }
695
+
696
+ tmpCtx.fillStyle = tmpInSelection ? tmpAccentColor : tmpSecondaryColor;
697
+ tmpCtx.fillRect(x, tmpTopY, 1, tmpBarHeight);
698
+ }
699
+
700
+ // Draw cursor
701
+ if (this._cursorX >= 0 && this._cursorX < tmpW)
702
+ {
703
+ tmpCtx.strokeStyle = '#ffffff44';
704
+ tmpCtx.lineWidth = 1;
705
+ tmpCtx.beginPath();
706
+ tmpCtx.moveTo(this._cursorX * tmpDPR, 0);
707
+ tmpCtx.lineTo(this._cursorX * tmpDPR, tmpH);
708
+ tmpCtx.stroke();
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Draw the overview (full waveform) canvas.
714
+ */
715
+ _drawOverviewWaveform()
716
+ {
717
+ if (!this._overviewCanvas || this._peaks.length === 0)
718
+ {
719
+ return;
720
+ }
721
+
722
+ let tmpCtx = this._overviewCanvas.getContext('2d');
723
+ let tmpW = this._overviewCanvas.width;
724
+ let tmpH = this._overviewCanvas.height;
725
+
726
+ tmpCtx.clearRect(0, 0, tmpW, tmpH);
727
+
728
+ let tmpBgColor = getComputedStyle(document.documentElement).getPropertyValue('--retold-bg-tertiary').trim() || '#313244';
729
+ tmpCtx.fillStyle = tmpBgColor;
730
+ tmpCtx.fillRect(0, 0, tmpW, tmpH);
731
+
732
+ let tmpPeaks = this._peaks;
733
+ let tmpTotalPeaks = tmpPeaks.length;
734
+ let tmpMidY = tmpH / 2;
735
+ let tmpSecondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--retold-text-muted').trim() || '#a6adc8';
736
+
737
+ // Draw selection in overview
738
+ if (this._selectionStart >= 0 && this._selectionEnd >= 0)
739
+ {
740
+ let tmpSelStartNorm = Math.min(this._selectionStart, this._selectionEnd);
741
+ let tmpSelEndNorm = Math.max(this._selectionStart, this._selectionEnd);
742
+ let tmpAccent = getComputedStyle(document.documentElement).getPropertyValue('--retold-accent').trim() || '#89b4fa';
743
+
744
+ tmpCtx.fillStyle = tmpAccent + '33';
745
+ tmpCtx.fillRect(tmpSelStartNorm * tmpW, 0, (tmpSelEndNorm - tmpSelStartNorm) * tmpW, tmpH);
746
+ }
747
+
748
+ for (let x = 0; x < tmpW; x++)
749
+ {
750
+ let tmpPeakStartF = (x / tmpW) * tmpTotalPeaks;
751
+ let tmpPeakEndF = ((x + 1) / tmpW) * tmpTotalPeaks;
752
+ let tmpPStart = Math.floor(tmpPeakStartF);
753
+ let tmpPEnd = Math.ceil(tmpPeakEndF);
754
+ tmpPStart = Math.max(0, Math.min(tmpPStart, tmpTotalPeaks - 1));
755
+ tmpPEnd = Math.max(tmpPStart + 1, Math.min(tmpPEnd, tmpTotalPeaks));
756
+
757
+ let tmpMin = 0;
758
+ let tmpMax = 0;
759
+ for (let p = tmpPStart; p < tmpPEnd; p++)
760
+ {
761
+ if (tmpPeaks[p].Min < tmpMin) tmpMin = tmpPeaks[p].Min;
762
+ if (tmpPeaks[p].Max > tmpMax) tmpMax = tmpPeaks[p].Max;
763
+ }
764
+
765
+ let tmpTopY = tmpMidY - (tmpMax * tmpMidY * 0.85);
766
+ let tmpBottomY = tmpMidY - (tmpMin * tmpMidY * 0.85);
767
+ let tmpBarHeight = Math.max(1, tmpBottomY - tmpTopY);
768
+
769
+ tmpCtx.fillStyle = tmpSecondaryColor;
770
+ tmpCtx.fillRect(x, tmpTopY, 1, tmpBarHeight);
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Update the overview viewport indicator position.
776
+ */
777
+ _updateOverviewViewport()
778
+ {
779
+ let tmpViewport = document.getElementById('RetoldRemote-AEX-OverviewViewport');
780
+ if (!tmpViewport)
781
+ {
782
+ return;
783
+ }
784
+
785
+ let tmpWrap = document.getElementById('RetoldRemote-AEX-OverviewWrap');
786
+ if (!tmpWrap)
787
+ {
788
+ return;
789
+ }
790
+
791
+ let tmpWidth = tmpWrap.clientWidth;
792
+ let tmpLeft = this._viewStart * tmpWidth;
793
+ let tmpRight = this._viewEnd * tmpWidth;
794
+
795
+ tmpViewport.style.left = tmpLeft + 'px';
796
+ tmpViewport.style.width = (tmpRight - tmpLeft) + 'px';
797
+ }
798
+
799
+ /**
800
+ * Bind mouse/touch events to the main canvas for interaction.
801
+ */
802
+ _bindCanvasEvents()
803
+ {
804
+ let tmpSelf = this;
805
+ let tmpWrap = document.getElementById('RetoldRemote-AEX-CanvasWrap');
806
+ if (!tmpWrap)
807
+ {
808
+ return;
809
+ }
810
+
811
+ // Mouse move for cursor display
812
+ tmpWrap.addEventListener('mousemove', (pEvent) =>
813
+ {
814
+ let tmpRect = tmpWrap.getBoundingClientRect();
815
+ tmpSelf._cursorX = pEvent.clientX - tmpRect.left;
816
+
817
+ if (tmpSelf._isDragging)
818
+ {
819
+ let tmpNorm = tmpSelf._viewStart + (tmpSelf._cursorX / tmpRect.width) * (tmpSelf._viewEnd - tmpSelf._viewStart);
820
+ tmpNorm = Math.max(0, Math.min(1, tmpNorm));
821
+ tmpSelf._selectionEnd = tmpNorm;
822
+ tmpSelf._drawMainWaveform();
823
+ tmpSelf._drawOverviewWaveform();
824
+ tmpSelf._updateSelectionButtons();
825
+ }
826
+
827
+ tmpSelf._updateTimeDisplay();
828
+ // Lightweight cursor redraw
829
+ tmpSelf._drawMainWaveform();
830
+ });
831
+
832
+ tmpWrap.addEventListener('mouseleave', () =>
833
+ {
834
+ tmpSelf._cursorX = -1;
835
+ tmpSelf._drawMainWaveform();
836
+ });
837
+
838
+ // Mouse down: start selection drag
839
+ tmpWrap.addEventListener('mousedown', (pEvent) =>
840
+ {
841
+ if (pEvent.button !== 0)
842
+ {
843
+ return;
844
+ }
845
+ let tmpRect = tmpWrap.getBoundingClientRect();
846
+ let tmpNorm = tmpSelf._viewStart + ((pEvent.clientX - tmpRect.left) / tmpRect.width) * (tmpSelf._viewEnd - tmpSelf._viewStart);
847
+ tmpNorm = Math.max(0, Math.min(1, tmpNorm));
848
+
849
+ tmpSelf._isDragging = true;
850
+ tmpSelf._selectionStart = tmpNorm;
851
+ tmpSelf._selectionEnd = tmpNorm;
852
+ tmpSelf._dragStart = tmpNorm;
853
+ tmpSelf._segmentURL = null;
854
+ let tmpPlaybackBar = document.getElementById('RetoldRemote-AEX-Playback');
855
+ if (tmpPlaybackBar)
856
+ {
857
+ tmpPlaybackBar.style.display = 'none';
858
+ }
859
+ });
860
+
861
+ // Mouse up: end selection drag
862
+ window.addEventListener('mouseup', () =>
863
+ {
864
+ if (tmpSelf._isDragging)
865
+ {
866
+ tmpSelf._isDragging = false;
867
+ // If selection is too small, clear it
868
+ if (Math.abs(tmpSelf._selectionEnd - tmpSelf._selectionStart) < 0.001)
869
+ {
870
+ tmpSelf._selectionStart = -1;
871
+ tmpSelf._selectionEnd = -1;
872
+ }
873
+ tmpSelf._updateSelectionButtons();
874
+ tmpSelf._drawAll();
875
+ tmpSelf._updateTimeDisplay();
876
+ }
877
+ });
878
+
879
+ // Mouse wheel: zoom
880
+ tmpWrap.addEventListener('wheel', (pEvent) =>
881
+ {
882
+ pEvent.preventDefault();
883
+ let tmpRect = tmpWrap.getBoundingClientRect();
884
+ let tmpMouseNorm = (pEvent.clientX - tmpRect.left) / tmpRect.width;
885
+
886
+ let tmpZoomFactor = pEvent.deltaY > 0 ? 1.2 : 0.8;
887
+ tmpSelf._zoomAtPoint(tmpMouseNorm, tmpZoomFactor);
888
+ }, { passive: false });
889
+
890
+ // Overview click: pan to position
891
+ let tmpOverviewWrap = document.getElementById('RetoldRemote-AEX-OverviewWrap');
892
+ if (tmpOverviewWrap)
893
+ {
894
+ tmpOverviewWrap.addEventListener('click', (pEvent) =>
895
+ {
896
+ let tmpRect = tmpOverviewWrap.getBoundingClientRect();
897
+ let tmpClickNorm = (pEvent.clientX - tmpRect.left) / tmpRect.width;
898
+ tmpClickNorm = Math.max(0, Math.min(1, tmpClickNorm));
899
+
900
+ let tmpViewRange = tmpSelf._viewEnd - tmpSelf._viewStart;
901
+ let tmpNewStart = tmpClickNorm - tmpViewRange / 2;
902
+ tmpNewStart = Math.max(0, Math.min(1 - tmpViewRange, tmpNewStart));
903
+
904
+ tmpSelf._viewStart = tmpNewStart;
905
+ tmpSelf._viewEnd = tmpNewStart + tmpViewRange;
906
+
907
+ tmpSelf._drawAll();
908
+ tmpSelf._updateTimeDisplay();
909
+ });
910
+ }
911
+ }
912
+
913
+ /**
914
+ * Zoom in/out centered on a normalized position within the current view.
915
+ *
916
+ * @param {number} pCenterNorm - Position within current view (0..1)
917
+ * @param {number} pFactor - Zoom factor (< 1 = zoom in, > 1 = zoom out)
918
+ */
919
+ _zoomAtPoint(pCenterNorm, pFactor)
920
+ {
921
+ let tmpViewRange = this._viewEnd - this._viewStart;
922
+ let tmpCenter = this._viewStart + pCenterNorm * tmpViewRange;
923
+
924
+ let tmpNewRange = tmpViewRange * pFactor;
925
+ tmpNewRange = Math.max(this._minZoom, Math.min(1, tmpNewRange));
926
+
927
+ let tmpNewStart = tmpCenter - pCenterNorm * tmpNewRange;
928
+ let tmpNewEnd = tmpNewStart + tmpNewRange;
929
+
930
+ // Clamp to 0..1
931
+ if (tmpNewStart < 0)
932
+ {
933
+ tmpNewStart = 0;
934
+ tmpNewEnd = tmpNewRange;
935
+ }
936
+ if (tmpNewEnd > 1)
937
+ {
938
+ tmpNewEnd = 1;
939
+ tmpNewStart = 1 - tmpNewRange;
940
+ }
941
+
942
+ this._viewStart = Math.max(0, tmpNewStart);
943
+ this._viewEnd = Math.min(1, tmpNewEnd);
944
+
945
+ this._drawAll();
946
+ this._updateTimeDisplay();
947
+ }
948
+
949
+ /**
950
+ * Update the time display bar with current view/selection/cursor info.
951
+ */
952
+ _updateTimeDisplay()
953
+ {
954
+ if (!this._waveformData)
955
+ {
956
+ return;
957
+ }
958
+
959
+ let tmpDuration = this._waveformData.Duration;
960
+
961
+ // View range
962
+ let tmpViewRangeEl = document.getElementById('RetoldRemote-AEX-ViewRange');
963
+ if (tmpViewRangeEl)
964
+ {
965
+ let tmpViewStartTime = this._viewStart * tmpDuration;
966
+ let tmpViewEndTime = this._viewEnd * tmpDuration;
967
+ tmpViewRangeEl.textContent = this._formatTimestamp(tmpViewStartTime) + ' - ' + this._formatTimestamp(tmpViewEndTime);
968
+ }
969
+
970
+ // Selection
971
+ let tmpSelRangeEl = document.getElementById('RetoldRemote-AEX-SelectionRange');
972
+ if (tmpSelRangeEl)
973
+ {
974
+ if (this._selectionStart >= 0 && this._selectionEnd >= 0)
975
+ {
976
+ let tmpSelMin = Math.min(this._selectionStart, this._selectionEnd) * tmpDuration;
977
+ let tmpSelMax = Math.max(this._selectionStart, this._selectionEnd) * tmpDuration;
978
+ let tmpSelDur = tmpSelMax - tmpSelMin;
979
+ tmpSelRangeEl.textContent = this._formatTimestamp(tmpSelMin) + ' - ' + this._formatTimestamp(tmpSelMax) + ' (' + this._formatTimestamp(tmpSelDur) + ')';
980
+ }
981
+ else
982
+ {
983
+ tmpSelRangeEl.textContent = 'None';
984
+ }
985
+ }
986
+
987
+ // Cursor
988
+ let tmpCursorEl = document.getElementById('RetoldRemote-AEX-CursorTime');
989
+ if (tmpCursorEl)
990
+ {
991
+ if (this._cursorX >= 0 && this._mainCanvas)
992
+ {
993
+ let tmpWrap = this._mainCanvas.parentElement;
994
+ if (tmpWrap)
995
+ {
996
+ let tmpNorm = this._viewStart + (this._cursorX / tmpWrap.clientWidth) * (this._viewEnd - this._viewStart);
997
+ tmpCursorEl.textContent = this._formatTimestamp(tmpNorm * tmpDuration);
998
+ }
999
+ }
1000
+ else
1001
+ {
1002
+ tmpCursorEl.textContent = '--';
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ /**
1008
+ * Update selection-dependent button states.
1009
+ */
1010
+ _updateSelectionButtons()
1011
+ {
1012
+ let tmpHasSelection = (this._selectionStart >= 0 && this._selectionEnd >= 0
1013
+ && Math.abs(this._selectionEnd - this._selectionStart) >= 0.001);
1014
+
1015
+ let tmpZoomSelBtn = document.getElementById('RetoldRemote-AEX-ZoomSelBtn');
1016
+ if (tmpZoomSelBtn)
1017
+ {
1018
+ tmpZoomSelBtn.disabled = !tmpHasSelection;
1019
+ }
1020
+
1021
+ let tmpPlaySelBtn = document.getElementById('RetoldRemote-AEX-PlaySelBtn');
1022
+ if (tmpPlaySelBtn)
1023
+ {
1024
+ tmpPlaySelBtn.disabled = !tmpHasSelection;
1025
+ }
1026
+ }
1027
+
1028
+ // --- User actions ---
1029
+
1030
+ zoomIn()
1031
+ {
1032
+ this._zoomAtPoint(0.5, 0.5);
1033
+ }
1034
+
1035
+ zoomOut()
1036
+ {
1037
+ this._zoomAtPoint(0.5, 2);
1038
+ }
1039
+
1040
+ zoomToFit()
1041
+ {
1042
+ this._viewStart = 0;
1043
+ this._viewEnd = 1;
1044
+ this._drawAll();
1045
+ this._updateTimeDisplay();
1046
+ }
1047
+
1048
+ zoomToSelection()
1049
+ {
1050
+ if (this._selectionStart < 0 || this._selectionEnd < 0)
1051
+ {
1052
+ return;
1053
+ }
1054
+
1055
+ let tmpMin = Math.min(this._selectionStart, this._selectionEnd);
1056
+ let tmpMax = Math.max(this._selectionStart, this._selectionEnd);
1057
+
1058
+ // Add a small margin (5%)
1059
+ let tmpRange = tmpMax - tmpMin;
1060
+ let tmpMargin = tmpRange * 0.05;
1061
+ this._viewStart = Math.max(0, tmpMin - tmpMargin);
1062
+ this._viewEnd = Math.min(1, tmpMax + tmpMargin);
1063
+
1064
+ this._drawAll();
1065
+ this._updateTimeDisplay();
1066
+ }
1067
+
1068
+ clearSelection()
1069
+ {
1070
+ this._selectionStart = -1;
1071
+ this._selectionEnd = -1;
1072
+ this._segmentURL = null;
1073
+
1074
+ let tmpPlaybackBar = document.getElementById('RetoldRemote-AEX-Playback');
1075
+ if (tmpPlaybackBar)
1076
+ {
1077
+ tmpPlaybackBar.style.display = 'none';
1078
+ }
1079
+
1080
+ this._updateSelectionButtons();
1081
+ this._drawAll();
1082
+ this._updateTimeDisplay();
1083
+ }
1084
+
1085
+ /**
1086
+ * Request the server to extract and serve the selected audio segment.
1087
+ */
1088
+ playSelection()
1089
+ {
1090
+ if (this._selectionStart < 0 || this._selectionEnd < 0 || !this._waveformData)
1091
+ {
1092
+ return;
1093
+ }
1094
+
1095
+ let tmpSelf = this;
1096
+ let tmpDuration = this._waveformData.Duration;
1097
+ let tmpStart = Math.min(this._selectionStart, this._selectionEnd) * tmpDuration;
1098
+ let tmpEnd = Math.max(this._selectionStart, this._selectionEnd) * tmpDuration;
1099
+
1100
+ let tmpProvider = this.pict.providers['RetoldRemote-Provider'];
1101
+ let tmpPathParam = tmpProvider ? tmpProvider._getPathParam(this._currentPath) : encodeURIComponent(this._currentPath);
1102
+
1103
+ let tmpURL = '/api/media/audio-segment?path=' + tmpPathParam
1104
+ + '&start=' + tmpStart.toFixed(3)
1105
+ + '&end=' + tmpEnd.toFixed(3)
1106
+ + '&format=mp3';
1107
+
1108
+ // Show playback bar with loading state
1109
+ let tmpPlaybackBar = document.getElementById('RetoldRemote-AEX-Playback');
1110
+ if (tmpPlaybackBar)
1111
+ {
1112
+ tmpPlaybackBar.style.display = '';
1113
+ }
1114
+
1115
+ let tmpAudioEl = document.getElementById('RetoldRemote-AEX-Audio');
1116
+ if (tmpAudioEl)
1117
+ {
1118
+ tmpAudioEl.src = tmpURL;
1119
+ tmpAudioEl.load();
1120
+ tmpAudioEl.play().catch(() =>
1121
+ {
1122
+ // Autoplay may be blocked; user can click play
1123
+ });
1124
+ }
1125
+
1126
+ this._segmentURL = tmpURL;
1127
+ }
1128
+
1129
+ /**
1130
+ * Navigate back to the audio player viewer.
1131
+ */
1132
+ goBack()
1133
+ {
1134
+ // Clean up resize observer
1135
+ if (this._resizeObserver)
1136
+ {
1137
+ this._resizeObserver.disconnect();
1138
+ this._resizeObserver = null;
1139
+ }
1140
+
1141
+ if (this._currentPath)
1142
+ {
1143
+ let tmpViewer = this.pict.views['RetoldRemote-MediaViewer'];
1144
+ if (tmpViewer)
1145
+ {
1146
+ tmpViewer.showMedia(this._currentPath, 'audio');
1147
+ }
1148
+ }
1149
+ else
1150
+ {
1151
+ let tmpNav = this.pict.providers['RetoldRemote-GalleryNavigation'];
1152
+ if (tmpNav)
1153
+ {
1154
+ tmpNav.closeViewer();
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ /**
1160
+ * Show an error message.
1161
+ *
1162
+ * @param {string} pMessage - Error message
1163
+ */
1164
+ _showError(pMessage)
1165
+ {
1166
+ let tmpBody = document.getElementById('RetoldRemote-AEX-Body');
1167
+ if (tmpBody)
1168
+ {
1169
+ tmpBody.innerHTML = '<div class="retold-remote-aex-error">'
1170
+ + '<div class="retold-remote-aex-error-message">' + this._escapeHTML(pMessage || 'An error occurred.') + '</div>'
1171
+ + '<button class="retold-remote-aex-nav-btn" onclick="pict.views[\'RetoldRemote-AudioExplorer\'].goBack()">Back to Audio</button>'
1172
+ + '</div>';
1173
+ }
1174
+ }
1175
+
1176
+ _formatTimestamp(pSeconds)
1177
+ {
1178
+ if (pSeconds === null || pSeconds === undefined || isNaN(pSeconds))
1179
+ {
1180
+ return '--';
1181
+ }
1182
+ let tmpHours = Math.floor(pSeconds / 3600);
1183
+ let tmpMinutes = Math.floor((pSeconds % 3600) / 60);
1184
+ let tmpSecs = Math.floor(pSeconds % 60);
1185
+ let tmpMs = Math.floor((pSeconds % 1) * 10);
1186
+
1187
+ if (tmpHours > 0)
1188
+ {
1189
+ return `${tmpHours}:${String(tmpMinutes).padStart(2, '0')}:${String(tmpSecs).padStart(2, '0')}.${tmpMs}`;
1190
+ }
1191
+ return `${tmpMinutes}:${String(tmpSecs).padStart(2, '0')}.${tmpMs}`;
1192
+ }
1193
+
1194
+ _escapeHTML(pText)
1195
+ {
1196
+ if (!pText) return '';
1197
+ return pText.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1198
+ }
1199
+
1200
+ _formatFileSize(pBytes)
1201
+ {
1202
+ if (!pBytes || pBytes === 0) return '0 B';
1203
+ let tmpUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
1204
+ let tmpIndex = Math.floor(Math.log(pBytes) / Math.log(1024));
1205
+ if (tmpIndex >= tmpUnits.length) tmpIndex = tmpUnits.length - 1;
1206
+ let tmpSize = pBytes / Math.pow(1024, tmpIndex);
1207
+ return tmpSize.toFixed(tmpIndex === 0 ? 0 : 1) + ' ' + tmpUnits[tmpIndex];
1208
+ }
1209
+ }
1210
+
1211
+ RetoldRemoteAudioExplorerView.default_configuration = _ViewConfiguration;
1212
+
1213
+ module.exports = RetoldRemoteAudioExplorerView;