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.
- package/html/index.html +2 -0
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +46 -5
- package/source/cli/RetoldRemote-CLI-Run.js +0 -0
- package/source/cli/RetoldRemote-Server-Setup.js +790 -8
- package/source/cli/commands/RetoldRemote-Command-Serve.js +34 -1
- package/source/providers/Pict-Provider-GalleryFilterSort.js +61 -9
- package/source/providers/Pict-Provider-GalleryNavigation.js +517 -18
- package/source/providers/Pict-Provider-RetoldRemote.js +11 -2
- package/source/providers/Pict-Provider-RetoldRemoteIcons.js +1 -0
- package/source/server/RetoldRemote-ArchiveService.js +830 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +673 -0
- package/source/server/RetoldRemote-EbookService.js +242 -0
- package/source/server/RetoldRemote-MediaService.js +1 -1
- package/source/server/RetoldRemote-ToolDetector.js +31 -1
- package/source/server/RetoldRemote-VideoFrameService.js +486 -0
- package/source/views/PictView-Remote-AudioExplorer.js +1213 -0
- package/source/views/PictView-Remote-Gallery.js +141 -2
- package/source/views/PictView-Remote-Layout.js +18 -27
- package/source/views/PictView-Remote-MediaViewer.js +638 -39
- package/source/views/PictView-Remote-SettingsPanel.js +23 -0
- package/source/views/PictView-Remote-TopBar.js +121 -0
- package/source/views/PictView-Remote-VideoExplorer.js +1229 -0
- package/web-application/index.html +2 -0
- package/web-application/js/epub.min.js +1 -0
- package/web-application/retold-remote.js +7030 -1244
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +13 -44
- package/web-application/retold-remote.min.js.map +1 -1
- package/web-application/retold-remote.compatible.js +0 -5764
- package/web-application/retold-remote.compatible.js.map +0 -1
- package/web-application/retold-remote.compatible.min.js +0 -120
- 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)">← Back</button>';
|
|
345
|
+
tmpHTML += '<div class="retold-remote-aex-title">Audio Explorer — ' + 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>▶ 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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;
|