chart-preview 1.0.1 → 1.0.3
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/CHANGELOG.md +12 -0
- package/dist/index.js +53 -64
- package/dist/index.mjs +53 -64
- package/package.json +1 -1
- package/src/ChartPreview.ts +70 -75
- package/src/ChartPreviewPlayer.ts +7 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# chart-preview
|
|
2
2
|
|
|
3
|
+
## 1.0.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 467db29: Enable play button in ChartPreviewPlayer by removing the disabled attribute, allowing user interaction.
|
|
8
|
+
|
|
9
|
+
## 1.0.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 5319db5: Refactor AudioManager to improve audio playback control and state management. Introduce independent paused state tracking, enhance audio source management, and streamline audio decoding process. Update ChartPreviewPlayer styles for better UI responsiveness and remove idle message.
|
|
14
|
+
|
|
3
15
|
## 1.0.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -330,11 +330,13 @@ var ChartRenderer = class extends THREE.WebGLRenderer {
|
|
|
330
330
|
}
|
|
331
331
|
};
|
|
332
332
|
var AudioManager = class {
|
|
333
|
+
// Track paused state independently since we share the AudioContext
|
|
333
334
|
constructor(audioFiles, startDelayMs, fallbackAudioLengthMs) {
|
|
334
335
|
this.audioFiles = audioFiles;
|
|
335
336
|
this.startDelayMs = startDelayMs;
|
|
336
337
|
this.fallbackAudioLengthMs = fallbackAudioLengthMs;
|
|
337
338
|
this.eventEmitter = new import_eventemitter3.EventEmitter();
|
|
339
|
+
this.audioBuffers = [];
|
|
338
340
|
this.gainNode = null;
|
|
339
341
|
this.audioSources = [];
|
|
340
342
|
this._volume = 0.5;
|
|
@@ -343,6 +345,7 @@ var AudioManager = class {
|
|
|
343
345
|
// Necessary because audioCtx.currentTime doesn't reset to 0 on seek
|
|
344
346
|
this.audioLengthMs = 0;
|
|
345
347
|
this.endEventTimeout = null;
|
|
348
|
+
this._isPaused = true;
|
|
346
349
|
const ctx = getSharedAudioContext();
|
|
347
350
|
if (!ctx) {
|
|
348
351
|
throw new Error("AudioContext is not supported in this environment");
|
|
@@ -361,7 +364,7 @@ var AudioManager = class {
|
|
|
361
364
|
startDelayMs,
|
|
362
365
|
fallbackAudioLengthMs
|
|
363
366
|
);
|
|
364
|
-
await audioManager.
|
|
367
|
+
await audioManager.decodeAudioFiles();
|
|
365
368
|
return audioManager;
|
|
366
369
|
}
|
|
367
370
|
/**
|
|
@@ -385,10 +388,12 @@ var AudioManager = class {
|
|
|
385
388
|
}
|
|
386
389
|
/** Nonnegative number of milliseconds representing time elapsed since the chart preview start. */
|
|
387
390
|
get chartCurrentTimeMs() {
|
|
388
|
-
|
|
391
|
+
if (this._isPaused) {
|
|
392
|
+
return this.lastSeekChartTimeMs;
|
|
393
|
+
}
|
|
389
394
|
const audioLatency = (this.audioCtx.baseLatency + (this.audioCtx.outputLatency || 0)) * 1e3;
|
|
390
395
|
const audioTimeSinceLastSeekMs = (this.audioCtx.currentTime - this.lastAudioCtxCurrentTime) * 1e3;
|
|
391
|
-
return this.lastSeekChartTimeMs + audioTimeSinceLastSeekMs -
|
|
396
|
+
return this.lastSeekChartTimeMs + audioTimeSinceLastSeekMs - audioLatency;
|
|
392
397
|
}
|
|
393
398
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
394
399
|
get chartEndTimeMs() {
|
|
@@ -401,13 +406,12 @@ var AudioManager = class {
|
|
|
401
406
|
);
|
|
402
407
|
}
|
|
403
408
|
async play() {
|
|
404
|
-
if (this.gainNode === null) {
|
|
405
|
-
await this.initAudio();
|
|
406
|
-
}
|
|
407
409
|
if (this.audioCtx.state === "suspended") {
|
|
408
410
|
await this.audioCtx.resume();
|
|
409
411
|
}
|
|
410
|
-
|
|
412
|
+
this.startAudioSources();
|
|
413
|
+
this._isPaused = false;
|
|
414
|
+
if (this.audioBuffers.length === 0 && !this.endEventTimeout) {
|
|
411
415
|
const remainingMs = this.chartEndTimeMs - this.chartCurrentTimeMs;
|
|
412
416
|
if (remainingMs > 0) {
|
|
413
417
|
this.endEventTimeout = setTimeout(() => {
|
|
@@ -417,20 +421,42 @@ var AudioManager = class {
|
|
|
417
421
|
}
|
|
418
422
|
}
|
|
419
423
|
async pause() {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
424
|
+
const currentTime = this.chartCurrentTimeMs;
|
|
425
|
+
this._isPaused = true;
|
|
426
|
+
this.lastSeekChartTimeMs = currentTime;
|
|
427
|
+
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
423
428
|
if (this.endEventTimeout) {
|
|
424
429
|
clearTimeout(this.endEventTimeout);
|
|
425
430
|
this.endEventTimeout = null;
|
|
426
431
|
}
|
|
432
|
+
this.stopAudioSources();
|
|
427
433
|
}
|
|
428
434
|
closeAudio() {
|
|
429
435
|
this.eventEmitter.removeAllListeners();
|
|
436
|
+
this._isPaused = true;
|
|
430
437
|
if (this.endEventTimeout) {
|
|
431
438
|
clearTimeout(this.endEventTimeout);
|
|
432
439
|
this.endEventTimeout = null;
|
|
433
440
|
}
|
|
441
|
+
this.stopAudioSources();
|
|
442
|
+
unregisterInstance();
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Decodes audio files into AudioBuffers. Called once during initialization.
|
|
446
|
+
*/
|
|
447
|
+
async decodeAudioFiles() {
|
|
448
|
+
this.audioBuffers = await Promise.all(
|
|
449
|
+
this.audioFiles.map(
|
|
450
|
+
(file) => this.audioCtx.decodeAudioData(file.slice(0).buffer)
|
|
451
|
+
)
|
|
452
|
+
);
|
|
453
|
+
const calculatedLengthMs = this.audioBuffers.length > 0 ? Math.max(...this.audioBuffers.map((b) => b.duration)) * 1e3 : 0;
|
|
454
|
+
this.audioLengthMs = calculatedLengthMs > 0 ? calculatedLengthMs : this.fallbackAudioLengthMs;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Stops all currently playing audio sources for this instance.
|
|
458
|
+
*/
|
|
459
|
+
stopAudioSources() {
|
|
434
460
|
for (const source of this.audioSources) {
|
|
435
461
|
source.onended = null;
|
|
436
462
|
try {
|
|
@@ -442,32 +468,18 @@ var AudioManager = class {
|
|
|
442
468
|
this.audioSources = [];
|
|
443
469
|
this.gainNode?.disconnect();
|
|
444
470
|
this.gainNode = null;
|
|
445
|
-
unregisterInstance();
|
|
446
471
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
source.disconnect();
|
|
453
|
-
} catch {
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
this.audioSources = [];
|
|
457
|
-
const audioBuffers = await Promise.all(
|
|
458
|
-
// Must be recreated on each seek because seek is not supported by the Web Audio API
|
|
459
|
-
this.audioFiles.map(
|
|
460
|
-
(file) => this.audioCtx.decodeAudioData(file.slice(0).buffer)
|
|
461
|
-
)
|
|
462
|
-
);
|
|
463
|
-
const calculatedLengthMs = audioBuffers.length > 0 ? Math.max(...audioBuffers.map((b) => b.duration)) * 1e3 : 0;
|
|
464
|
-
this.audioLengthMs = calculatedLengthMs > 0 ? calculatedLengthMs : this.fallbackAudioLengthMs;
|
|
472
|
+
/**
|
|
473
|
+
* Creates and starts audio sources from the current playback position.
|
|
474
|
+
*/
|
|
475
|
+
startAudioSources() {
|
|
476
|
+
this.stopAudioSources();
|
|
465
477
|
this.gainNode = this.audioCtx.createGain();
|
|
466
478
|
this.gainNode.gain.value = this._volume;
|
|
467
479
|
this.gainNode.connect(this.audioCtx.destination);
|
|
468
480
|
let endedCount = 0;
|
|
469
481
|
const audioStartOffsetSeconds = (this.lastSeekChartTimeMs - this.startDelayMs) / 1e3;
|
|
470
|
-
if (audioBuffers.length === 0) {
|
|
482
|
+
if (this.audioBuffers.length === 0) {
|
|
471
483
|
if (this.endEventTimeout) {
|
|
472
484
|
clearTimeout(this.endEventTimeout);
|
|
473
485
|
this.endEventTimeout = null;
|
|
@@ -479,12 +491,12 @@ var AudioManager = class {
|
|
|
479
491
|
}, remainingMs);
|
|
480
492
|
}
|
|
481
493
|
}
|
|
482
|
-
for (const audioBuffer of audioBuffers) {
|
|
494
|
+
for (const audioBuffer of this.audioBuffers) {
|
|
483
495
|
const source = this.audioCtx.createBufferSource();
|
|
484
496
|
source.buffer = audioBuffer;
|
|
485
497
|
source.onended = () => {
|
|
486
498
|
endedCount++;
|
|
487
|
-
if (endedCount === audioBuffers.length) {
|
|
499
|
+
if (endedCount === this.audioBuffers.length && !this._isPaused) {
|
|
488
500
|
this.eventEmitter.emit("end");
|
|
489
501
|
}
|
|
490
502
|
};
|
|
@@ -496,33 +508,20 @@ var AudioManager = class {
|
|
|
496
508
|
this.audioSources.push(source);
|
|
497
509
|
}
|
|
498
510
|
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
499
|
-
this.pause();
|
|
500
511
|
}
|
|
501
512
|
/**
|
|
502
513
|
* @param percentComplete The progress between the start and end of the preview.
|
|
503
514
|
*/
|
|
504
515
|
async seek(percentComplete) {
|
|
505
|
-
if (this.audioCtx.state === "running") {
|
|
506
|
-
await this.audioCtx.suspend();
|
|
507
|
-
}
|
|
508
516
|
if (this.endEventTimeout) {
|
|
509
517
|
clearTimeout(this.endEventTimeout);
|
|
510
518
|
this.endEventTimeout = null;
|
|
511
519
|
}
|
|
512
|
-
|
|
513
|
-
source.onended = null;
|
|
514
|
-
try {
|
|
515
|
-
source.stop();
|
|
516
|
-
source.disconnect();
|
|
517
|
-
} catch {
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
this.audioSources = [];
|
|
521
|
-
this.gainNode?.disconnect();
|
|
522
|
-
this.gainNode = null;
|
|
520
|
+
this.stopAudioSources();
|
|
523
521
|
const chartSeekTimeMs = percentComplete * this.chartEndTimeMs;
|
|
524
522
|
this.lastSeekChartTimeMs = chartSeekTimeMs;
|
|
525
523
|
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
524
|
+
this._isPaused = true;
|
|
526
525
|
}
|
|
527
526
|
};
|
|
528
527
|
var SilentAudioManager = class {
|
|
@@ -1269,7 +1268,6 @@ var STYLES = `
|
|
|
1269
1268
|
--accent-color: #3b82f6;
|
|
1270
1269
|
--text-color: #e0e0e0;
|
|
1271
1270
|
--text-muted: #aaa;
|
|
1272
|
-
--text-idle: #666;
|
|
1273
1271
|
--error-color: #ef4444;
|
|
1274
1272
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
|
1275
1273
|
|
|
@@ -1341,8 +1339,8 @@ var STYLES = `
|
|
|
1341
1339
|
align-items: center;
|
|
1342
1340
|
justify-content: center;
|
|
1343
1341
|
height: 40px;
|
|
1344
|
-
width:
|
|
1345
|
-
padding:
|
|
1342
|
+
width: 32px;
|
|
1343
|
+
padding: 6px;
|
|
1346
1344
|
transition: background 0.15s;
|
|
1347
1345
|
flex-shrink: 0;
|
|
1348
1346
|
}
|
|
@@ -1427,10 +1425,10 @@ var STYLES = `
|
|
|
1427
1425
|
.timestamp {
|
|
1428
1426
|
color: var(--text-muted);
|
|
1429
1427
|
font-size: 12px;
|
|
1430
|
-
padding: 0
|
|
1428
|
+
padding: 0 2px;
|
|
1431
1429
|
white-space: nowrap;
|
|
1432
1430
|
flex-shrink: 0;
|
|
1433
|
-
min-width:
|
|
1431
|
+
min-width: 70px;
|
|
1434
1432
|
text-align: center;
|
|
1435
1433
|
}
|
|
1436
1434
|
|
|
@@ -1438,7 +1436,7 @@ var STYLES = `
|
|
|
1438
1436
|
flex: 1;
|
|
1439
1437
|
display: flex;
|
|
1440
1438
|
align-items: center;
|
|
1441
|
-
padding: 0
|
|
1439
|
+
padding: 0 4px;
|
|
1442
1440
|
min-width: 0;
|
|
1443
1441
|
}
|
|
1444
1442
|
|
|
@@ -1480,11 +1478,6 @@ var STYLES = `
|
|
|
1480
1478
|
cursor: not-allowed;
|
|
1481
1479
|
}
|
|
1482
1480
|
|
|
1483
|
-
.idle-message {
|
|
1484
|
-
color: var(--text-idle);
|
|
1485
|
-
font-size: 14px;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
1481
|
.error-message {
|
|
1489
1482
|
color: var(--error-color);
|
|
1490
1483
|
font-size: 14px;
|
|
@@ -1589,10 +1582,9 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1589
1582
|
<style>${STYLES}</style>
|
|
1590
1583
|
<div class="player-container">
|
|
1591
1584
|
<div class="preview-viewport">
|
|
1592
|
-
<span class="idle-message">Click play to start</span>
|
|
1593
1585
|
</div>
|
|
1594
1586
|
<div class="controls">
|
|
1595
|
-
<button class="control-btn play-btn" aria-label="Play"
|
|
1587
|
+
<button class="control-btn play-btn" aria-label="Play">
|
|
1596
1588
|
${ICONS.play}
|
|
1597
1589
|
</button>
|
|
1598
1590
|
<div class="volume-wrapper">
|
|
@@ -1707,9 +1699,6 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1707
1699
|
*/
|
|
1708
1700
|
prepareForLoading() {
|
|
1709
1701
|
this.setState("loading");
|
|
1710
|
-
const idleMessage = this.viewport.querySelector(".idle-message");
|
|
1711
|
-
if (idleMessage)
|
|
1712
|
-
idleMessage.remove();
|
|
1713
1702
|
const errorMessage = this.viewport.querySelector(".error-message");
|
|
1714
1703
|
if (errorMessage)
|
|
1715
1704
|
errorMessage.remove();
|
|
@@ -1752,7 +1741,7 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1752
1741
|
updatePlayButton() {
|
|
1753
1742
|
switch (this._state) {
|
|
1754
1743
|
case "idle":
|
|
1755
|
-
this.playBtn.disabled =
|
|
1744
|
+
this.playBtn.disabled = false;
|
|
1756
1745
|
this.playBtn.innerHTML = ICONS.play;
|
|
1757
1746
|
this.playBtn.setAttribute("aria-label", "Play");
|
|
1758
1747
|
break;
|
package/dist/index.mjs
CHANGED
|
@@ -291,11 +291,13 @@ var ChartRenderer = class extends THREE.WebGLRenderer {
|
|
|
291
291
|
}
|
|
292
292
|
};
|
|
293
293
|
var AudioManager = class {
|
|
294
|
+
// Track paused state independently since we share the AudioContext
|
|
294
295
|
constructor(audioFiles, startDelayMs, fallbackAudioLengthMs) {
|
|
295
296
|
this.audioFiles = audioFiles;
|
|
296
297
|
this.startDelayMs = startDelayMs;
|
|
297
298
|
this.fallbackAudioLengthMs = fallbackAudioLengthMs;
|
|
298
299
|
this.eventEmitter = new EventEmitter();
|
|
300
|
+
this.audioBuffers = [];
|
|
299
301
|
this.gainNode = null;
|
|
300
302
|
this.audioSources = [];
|
|
301
303
|
this._volume = 0.5;
|
|
@@ -304,6 +306,7 @@ var AudioManager = class {
|
|
|
304
306
|
// Necessary because audioCtx.currentTime doesn't reset to 0 on seek
|
|
305
307
|
this.audioLengthMs = 0;
|
|
306
308
|
this.endEventTimeout = null;
|
|
309
|
+
this._isPaused = true;
|
|
307
310
|
const ctx = getSharedAudioContext();
|
|
308
311
|
if (!ctx) {
|
|
309
312
|
throw new Error("AudioContext is not supported in this environment");
|
|
@@ -322,7 +325,7 @@ var AudioManager = class {
|
|
|
322
325
|
startDelayMs,
|
|
323
326
|
fallbackAudioLengthMs
|
|
324
327
|
);
|
|
325
|
-
await audioManager.
|
|
328
|
+
await audioManager.decodeAudioFiles();
|
|
326
329
|
return audioManager;
|
|
327
330
|
}
|
|
328
331
|
/**
|
|
@@ -346,10 +349,12 @@ var AudioManager = class {
|
|
|
346
349
|
}
|
|
347
350
|
/** Nonnegative number of milliseconds representing time elapsed since the chart preview start. */
|
|
348
351
|
get chartCurrentTimeMs() {
|
|
349
|
-
|
|
352
|
+
if (this._isPaused) {
|
|
353
|
+
return this.lastSeekChartTimeMs;
|
|
354
|
+
}
|
|
350
355
|
const audioLatency = (this.audioCtx.baseLatency + (this.audioCtx.outputLatency || 0)) * 1e3;
|
|
351
356
|
const audioTimeSinceLastSeekMs = (this.audioCtx.currentTime - this.lastAudioCtxCurrentTime) * 1e3;
|
|
352
|
-
return this.lastSeekChartTimeMs + audioTimeSinceLastSeekMs -
|
|
357
|
+
return this.lastSeekChartTimeMs + audioTimeSinceLastSeekMs - audioLatency;
|
|
353
358
|
}
|
|
354
359
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
355
360
|
get chartEndTimeMs() {
|
|
@@ -362,13 +367,12 @@ var AudioManager = class {
|
|
|
362
367
|
);
|
|
363
368
|
}
|
|
364
369
|
async play() {
|
|
365
|
-
if (this.gainNode === null) {
|
|
366
|
-
await this.initAudio();
|
|
367
|
-
}
|
|
368
370
|
if (this.audioCtx.state === "suspended") {
|
|
369
371
|
await this.audioCtx.resume();
|
|
370
372
|
}
|
|
371
|
-
|
|
373
|
+
this.startAudioSources();
|
|
374
|
+
this._isPaused = false;
|
|
375
|
+
if (this.audioBuffers.length === 0 && !this.endEventTimeout) {
|
|
372
376
|
const remainingMs = this.chartEndTimeMs - this.chartCurrentTimeMs;
|
|
373
377
|
if (remainingMs > 0) {
|
|
374
378
|
this.endEventTimeout = setTimeout(() => {
|
|
@@ -378,20 +382,42 @@ var AudioManager = class {
|
|
|
378
382
|
}
|
|
379
383
|
}
|
|
380
384
|
async pause() {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
385
|
+
const currentTime = this.chartCurrentTimeMs;
|
|
386
|
+
this._isPaused = true;
|
|
387
|
+
this.lastSeekChartTimeMs = currentTime;
|
|
388
|
+
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
384
389
|
if (this.endEventTimeout) {
|
|
385
390
|
clearTimeout(this.endEventTimeout);
|
|
386
391
|
this.endEventTimeout = null;
|
|
387
392
|
}
|
|
393
|
+
this.stopAudioSources();
|
|
388
394
|
}
|
|
389
395
|
closeAudio() {
|
|
390
396
|
this.eventEmitter.removeAllListeners();
|
|
397
|
+
this._isPaused = true;
|
|
391
398
|
if (this.endEventTimeout) {
|
|
392
399
|
clearTimeout(this.endEventTimeout);
|
|
393
400
|
this.endEventTimeout = null;
|
|
394
401
|
}
|
|
402
|
+
this.stopAudioSources();
|
|
403
|
+
unregisterInstance();
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Decodes audio files into AudioBuffers. Called once during initialization.
|
|
407
|
+
*/
|
|
408
|
+
async decodeAudioFiles() {
|
|
409
|
+
this.audioBuffers = await Promise.all(
|
|
410
|
+
this.audioFiles.map(
|
|
411
|
+
(file) => this.audioCtx.decodeAudioData(file.slice(0).buffer)
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
const calculatedLengthMs = this.audioBuffers.length > 0 ? Math.max(...this.audioBuffers.map((b) => b.duration)) * 1e3 : 0;
|
|
415
|
+
this.audioLengthMs = calculatedLengthMs > 0 ? calculatedLengthMs : this.fallbackAudioLengthMs;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Stops all currently playing audio sources for this instance.
|
|
419
|
+
*/
|
|
420
|
+
stopAudioSources() {
|
|
395
421
|
for (const source of this.audioSources) {
|
|
396
422
|
source.onended = null;
|
|
397
423
|
try {
|
|
@@ -403,32 +429,18 @@ var AudioManager = class {
|
|
|
403
429
|
this.audioSources = [];
|
|
404
430
|
this.gainNode?.disconnect();
|
|
405
431
|
this.gainNode = null;
|
|
406
|
-
unregisterInstance();
|
|
407
432
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
source.disconnect();
|
|
414
|
-
} catch {
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
this.audioSources = [];
|
|
418
|
-
const audioBuffers = await Promise.all(
|
|
419
|
-
// Must be recreated on each seek because seek is not supported by the Web Audio API
|
|
420
|
-
this.audioFiles.map(
|
|
421
|
-
(file) => this.audioCtx.decodeAudioData(file.slice(0).buffer)
|
|
422
|
-
)
|
|
423
|
-
);
|
|
424
|
-
const calculatedLengthMs = audioBuffers.length > 0 ? Math.max(...audioBuffers.map((b) => b.duration)) * 1e3 : 0;
|
|
425
|
-
this.audioLengthMs = calculatedLengthMs > 0 ? calculatedLengthMs : this.fallbackAudioLengthMs;
|
|
433
|
+
/**
|
|
434
|
+
* Creates and starts audio sources from the current playback position.
|
|
435
|
+
*/
|
|
436
|
+
startAudioSources() {
|
|
437
|
+
this.stopAudioSources();
|
|
426
438
|
this.gainNode = this.audioCtx.createGain();
|
|
427
439
|
this.gainNode.gain.value = this._volume;
|
|
428
440
|
this.gainNode.connect(this.audioCtx.destination);
|
|
429
441
|
let endedCount = 0;
|
|
430
442
|
const audioStartOffsetSeconds = (this.lastSeekChartTimeMs - this.startDelayMs) / 1e3;
|
|
431
|
-
if (audioBuffers.length === 0) {
|
|
443
|
+
if (this.audioBuffers.length === 0) {
|
|
432
444
|
if (this.endEventTimeout) {
|
|
433
445
|
clearTimeout(this.endEventTimeout);
|
|
434
446
|
this.endEventTimeout = null;
|
|
@@ -440,12 +452,12 @@ var AudioManager = class {
|
|
|
440
452
|
}, remainingMs);
|
|
441
453
|
}
|
|
442
454
|
}
|
|
443
|
-
for (const audioBuffer of audioBuffers) {
|
|
455
|
+
for (const audioBuffer of this.audioBuffers) {
|
|
444
456
|
const source = this.audioCtx.createBufferSource();
|
|
445
457
|
source.buffer = audioBuffer;
|
|
446
458
|
source.onended = () => {
|
|
447
459
|
endedCount++;
|
|
448
|
-
if (endedCount === audioBuffers.length) {
|
|
460
|
+
if (endedCount === this.audioBuffers.length && !this._isPaused) {
|
|
449
461
|
this.eventEmitter.emit("end");
|
|
450
462
|
}
|
|
451
463
|
};
|
|
@@ -457,33 +469,20 @@ var AudioManager = class {
|
|
|
457
469
|
this.audioSources.push(source);
|
|
458
470
|
}
|
|
459
471
|
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
460
|
-
this.pause();
|
|
461
472
|
}
|
|
462
473
|
/**
|
|
463
474
|
* @param percentComplete The progress between the start and end of the preview.
|
|
464
475
|
*/
|
|
465
476
|
async seek(percentComplete) {
|
|
466
|
-
if (this.audioCtx.state === "running") {
|
|
467
|
-
await this.audioCtx.suspend();
|
|
468
|
-
}
|
|
469
477
|
if (this.endEventTimeout) {
|
|
470
478
|
clearTimeout(this.endEventTimeout);
|
|
471
479
|
this.endEventTimeout = null;
|
|
472
480
|
}
|
|
473
|
-
|
|
474
|
-
source.onended = null;
|
|
475
|
-
try {
|
|
476
|
-
source.stop();
|
|
477
|
-
source.disconnect();
|
|
478
|
-
} catch {
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
this.audioSources = [];
|
|
482
|
-
this.gainNode?.disconnect();
|
|
483
|
-
this.gainNode = null;
|
|
481
|
+
this.stopAudioSources();
|
|
484
482
|
const chartSeekTimeMs = percentComplete * this.chartEndTimeMs;
|
|
485
483
|
this.lastSeekChartTimeMs = chartSeekTimeMs;
|
|
486
484
|
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
485
|
+
this._isPaused = true;
|
|
487
486
|
}
|
|
488
487
|
};
|
|
489
488
|
var SilentAudioManager = class {
|
|
@@ -1234,7 +1233,6 @@ var STYLES = `
|
|
|
1234
1233
|
--accent-color: #3b82f6;
|
|
1235
1234
|
--text-color: #e0e0e0;
|
|
1236
1235
|
--text-muted: #aaa;
|
|
1237
|
-
--text-idle: #666;
|
|
1238
1236
|
--error-color: #ef4444;
|
|
1239
1237
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
|
1240
1238
|
|
|
@@ -1306,8 +1304,8 @@ var STYLES = `
|
|
|
1306
1304
|
align-items: center;
|
|
1307
1305
|
justify-content: center;
|
|
1308
1306
|
height: 40px;
|
|
1309
|
-
width:
|
|
1310
|
-
padding:
|
|
1307
|
+
width: 32px;
|
|
1308
|
+
padding: 6px;
|
|
1311
1309
|
transition: background 0.15s;
|
|
1312
1310
|
flex-shrink: 0;
|
|
1313
1311
|
}
|
|
@@ -1392,10 +1390,10 @@ var STYLES = `
|
|
|
1392
1390
|
.timestamp {
|
|
1393
1391
|
color: var(--text-muted);
|
|
1394
1392
|
font-size: 12px;
|
|
1395
|
-
padding: 0
|
|
1393
|
+
padding: 0 2px;
|
|
1396
1394
|
white-space: nowrap;
|
|
1397
1395
|
flex-shrink: 0;
|
|
1398
|
-
min-width:
|
|
1396
|
+
min-width: 70px;
|
|
1399
1397
|
text-align: center;
|
|
1400
1398
|
}
|
|
1401
1399
|
|
|
@@ -1403,7 +1401,7 @@ var STYLES = `
|
|
|
1403
1401
|
flex: 1;
|
|
1404
1402
|
display: flex;
|
|
1405
1403
|
align-items: center;
|
|
1406
|
-
padding: 0
|
|
1404
|
+
padding: 0 4px;
|
|
1407
1405
|
min-width: 0;
|
|
1408
1406
|
}
|
|
1409
1407
|
|
|
@@ -1445,11 +1443,6 @@ var STYLES = `
|
|
|
1445
1443
|
cursor: not-allowed;
|
|
1446
1444
|
}
|
|
1447
1445
|
|
|
1448
|
-
.idle-message {
|
|
1449
|
-
color: var(--text-idle);
|
|
1450
|
-
font-size: 14px;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
1446
|
.error-message {
|
|
1454
1447
|
color: var(--error-color);
|
|
1455
1448
|
font-size: 14px;
|
|
@@ -1554,10 +1547,9 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1554
1547
|
<style>${STYLES}</style>
|
|
1555
1548
|
<div class="player-container">
|
|
1556
1549
|
<div class="preview-viewport">
|
|
1557
|
-
<span class="idle-message">Click play to start</span>
|
|
1558
1550
|
</div>
|
|
1559
1551
|
<div class="controls">
|
|
1560
|
-
<button class="control-btn play-btn" aria-label="Play"
|
|
1552
|
+
<button class="control-btn play-btn" aria-label="Play">
|
|
1561
1553
|
${ICONS.play}
|
|
1562
1554
|
</button>
|
|
1563
1555
|
<div class="volume-wrapper">
|
|
@@ -1672,9 +1664,6 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1672
1664
|
*/
|
|
1673
1665
|
prepareForLoading() {
|
|
1674
1666
|
this.setState("loading");
|
|
1675
|
-
const idleMessage = this.viewport.querySelector(".idle-message");
|
|
1676
|
-
if (idleMessage)
|
|
1677
|
-
idleMessage.remove();
|
|
1678
1667
|
const errorMessage = this.viewport.querySelector(".error-message");
|
|
1679
1668
|
if (errorMessage)
|
|
1680
1669
|
errorMessage.remove();
|
|
@@ -1717,7 +1706,7 @@ var ChartPreviewPlayer = class extends HTMLElement {
|
|
|
1717
1706
|
updatePlayButton() {
|
|
1718
1707
|
switch (this._state) {
|
|
1719
1708
|
case "idle":
|
|
1720
|
-
this.playBtn.disabled =
|
|
1709
|
+
this.playBtn.disabled = false;
|
|
1721
1710
|
this.playBtn.innerHTML = ICONS.play;
|
|
1722
1711
|
this.playBtn.setAttribute("aria-label", "Play");
|
|
1723
1712
|
break;
|
package/package.json
CHANGED
package/src/ChartPreview.ts
CHANGED
|
@@ -387,6 +387,7 @@ class AudioManager {
|
|
|
387
387
|
private eventEmitter = new EventEmitter();
|
|
388
388
|
|
|
389
389
|
private audioCtx: AudioContext;
|
|
390
|
+
private audioBuffers: AudioBuffer[] = [];
|
|
390
391
|
private gainNode: GainNode | null = null;
|
|
391
392
|
private audioSources: AudioBufferSourceNode[] = [];
|
|
392
393
|
private _volume = 0.5;
|
|
@@ -394,6 +395,7 @@ class AudioManager {
|
|
|
394
395
|
private lastAudioCtxCurrentTime = 0; // Necessary because audioCtx.currentTime doesn't reset to 0 on seek
|
|
395
396
|
private audioLengthMs: number = 0;
|
|
396
397
|
private endEventTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
398
|
+
private _isPaused = true; // Track paused state independently since we share the AudioContext
|
|
397
399
|
|
|
398
400
|
private constructor(
|
|
399
401
|
private audioFiles: Uint8Array[],
|
|
@@ -423,7 +425,7 @@ class AudioManager {
|
|
|
423
425
|
startDelayMs,
|
|
424
426
|
fallbackAudioLengthMs
|
|
425
427
|
);
|
|
426
|
-
await audioManager.
|
|
428
|
+
await audioManager.decodeAudioFiles();
|
|
427
429
|
return audioManager;
|
|
428
430
|
}
|
|
429
431
|
|
|
@@ -453,20 +455,17 @@ class AudioManager {
|
|
|
453
455
|
|
|
454
456
|
/** Nonnegative number of milliseconds representing time elapsed since the chart preview start. */
|
|
455
457
|
get chartCurrentTimeMs() {
|
|
456
|
-
|
|
458
|
+
// Use our own paused state since we share the AudioContext with other instances
|
|
459
|
+
// and can't rely on audioCtx.state to determine this instance's state
|
|
460
|
+
if (this._isPaused) {
|
|
461
|
+
return this.lastSeekChartTimeMs;
|
|
462
|
+
}
|
|
457
463
|
// outputLatency is not implemented in safari
|
|
458
464
|
const audioLatency =
|
|
459
465
|
(this.audioCtx.baseLatency + (this.audioCtx.outputLatency || 0)) * 1000;
|
|
460
466
|
const audioTimeSinceLastSeekMs =
|
|
461
467
|
(this.audioCtx.currentTime - this.lastAudioCtxCurrentTime) * 1000;
|
|
462
|
-
|
|
463
|
-
// The solution here is to represent that visually by jumping ahead slightly by ignoring latency when paused.
|
|
464
|
-
// If this is a more significant problem, it can be fixed by seeking backward by `audioLatency`.
|
|
465
|
-
return (
|
|
466
|
-
this.lastSeekChartTimeMs +
|
|
467
|
-
audioTimeSinceLastSeekMs -
|
|
468
|
-
(isPaused ? 0 : audioLatency)
|
|
469
|
-
);
|
|
468
|
+
return this.lastSeekChartTimeMs + audioTimeSinceLastSeekMs - audioLatency;
|
|
470
469
|
}
|
|
471
470
|
|
|
472
471
|
/** Nonnegative number of milliseconds representing when the audio ends (and when the chart preview ends). */
|
|
@@ -484,16 +483,17 @@ class AudioManager {
|
|
|
484
483
|
}
|
|
485
484
|
|
|
486
485
|
async play() {
|
|
487
|
-
//
|
|
488
|
-
if (this.gainNode === null) {
|
|
489
|
-
await this.initAudio();
|
|
490
|
-
}
|
|
491
|
-
// Resume context if suspended
|
|
486
|
+
// Resume the shared context if suspended (needed for initial user interaction)
|
|
492
487
|
if (this.audioCtx.state === "suspended") {
|
|
493
488
|
await this.audioCtx.resume();
|
|
494
489
|
}
|
|
495
|
-
|
|
496
|
-
|
|
490
|
+
|
|
491
|
+
// Start audio playback
|
|
492
|
+
this.startAudioSources();
|
|
493
|
+
this._isPaused = false;
|
|
494
|
+
|
|
495
|
+
// Start end event timeout for charts without audio
|
|
496
|
+
if (this.audioBuffers.length === 0 && !this.endEventTimeout) {
|
|
497
497
|
const remainingMs = this.chartEndTimeMs - this.chartCurrentTimeMs;
|
|
498
498
|
if (remainingMs > 0) {
|
|
499
499
|
this.endEventTimeout = setTimeout(() => {
|
|
@@ -502,58 +502,44 @@ class AudioManager {
|
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
|
+
|
|
505
506
|
async pause() {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
507
|
+
// Record current playback position before stopping
|
|
508
|
+
const currentTime = this.chartCurrentTimeMs;
|
|
509
|
+
this._isPaused = true;
|
|
510
|
+
this.lastSeekChartTimeMs = currentTime;
|
|
511
|
+
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
512
|
+
|
|
509
513
|
// Clear end event timeout when pausing
|
|
510
514
|
if (this.endEventTimeout) {
|
|
511
515
|
clearTimeout(this.endEventTimeout);
|
|
512
516
|
this.endEventTimeout = null;
|
|
513
517
|
}
|
|
518
|
+
|
|
519
|
+
// Stop all audio sources for this instance only
|
|
520
|
+
// (Don't suspend the shared AudioContext - other instances may be playing)
|
|
521
|
+
this.stopAudioSources();
|
|
514
522
|
}
|
|
515
523
|
|
|
516
524
|
closeAudio() {
|
|
517
525
|
this.eventEmitter.removeAllListeners();
|
|
526
|
+
this._isPaused = true;
|
|
518
527
|
// Clear end event timeout
|
|
519
528
|
if (this.endEventTimeout) {
|
|
520
529
|
clearTimeout(this.endEventTimeout);
|
|
521
530
|
this.endEventTimeout = null;
|
|
522
531
|
}
|
|
523
532
|
// Stop all audio sources
|
|
524
|
-
|
|
525
|
-
// Remove onended handler before stopping to prevent false "end" events
|
|
526
|
-
source.onended = null;
|
|
527
|
-
try {
|
|
528
|
-
source.stop();
|
|
529
|
-
source.disconnect();
|
|
530
|
-
} catch {
|
|
531
|
-
// Source may already be stopped
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
this.audioSources = [];
|
|
535
|
-
this.gainNode?.disconnect();
|
|
536
|
-
this.gainNode = null;
|
|
533
|
+
this.stopAudioSources();
|
|
537
534
|
// Don't close the shared context - just unregister this instance
|
|
538
535
|
unregisterInstance();
|
|
539
536
|
}
|
|
540
537
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
source.stop();
|
|
548
|
-
source.disconnect();
|
|
549
|
-
} catch {
|
|
550
|
-
// Source may already be stopped
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
this.audioSources = [];
|
|
554
|
-
|
|
555
|
-
const audioBuffers = await Promise.all(
|
|
556
|
-
// Must be recreated on each seek because seek is not supported by the Web Audio API
|
|
538
|
+
/**
|
|
539
|
+
* Decodes audio files into AudioBuffers. Called once during initialization.
|
|
540
|
+
*/
|
|
541
|
+
private async decodeAudioFiles() {
|
|
542
|
+
this.audioBuffers = await Promise.all(
|
|
557
543
|
this.audioFiles.map((file) =>
|
|
558
544
|
this.audioCtx.decodeAudioData(file.slice(0).buffer)
|
|
559
545
|
)
|
|
@@ -561,11 +547,37 @@ class AudioManager {
|
|
|
561
547
|
|
|
562
548
|
// Use fallback if no audio files or if calculated duration is invalid
|
|
563
549
|
const calculatedLengthMs =
|
|
564
|
-
audioBuffers.length > 0
|
|
565
|
-
? Math.max(...audioBuffers.map((b) => b.duration)) * 1000
|
|
550
|
+
this.audioBuffers.length > 0
|
|
551
|
+
? Math.max(...this.audioBuffers.map((b) => b.duration)) * 1000
|
|
566
552
|
: 0;
|
|
567
553
|
this.audioLengthMs =
|
|
568
554
|
calculatedLengthMs > 0 ? calculatedLengthMs : this.fallbackAudioLengthMs;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Stops all currently playing audio sources for this instance.
|
|
559
|
+
*/
|
|
560
|
+
private stopAudioSources() {
|
|
561
|
+
for (const source of this.audioSources) {
|
|
562
|
+
source.onended = null; // Prevent false "end" events
|
|
563
|
+
try {
|
|
564
|
+
source.stop();
|
|
565
|
+
source.disconnect();
|
|
566
|
+
} catch {
|
|
567
|
+
// Source may already be stopped
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
this.audioSources = [];
|
|
571
|
+
this.gainNode?.disconnect();
|
|
572
|
+
this.gainNode = null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Creates and starts audio sources from the current playback position.
|
|
577
|
+
*/
|
|
578
|
+
private startAudioSources() {
|
|
579
|
+
// Stop any existing sources before creating new ones
|
|
580
|
+
this.stopAudioSources();
|
|
569
581
|
|
|
570
582
|
this.gainNode = this.audioCtx.createGain();
|
|
571
583
|
this.gainNode.gain.value = this._volume;
|
|
@@ -575,8 +587,8 @@ class AudioManager {
|
|
|
575
587
|
const audioStartOffsetSeconds =
|
|
576
588
|
(this.lastSeekChartTimeMs - this.startDelayMs) / 1000;
|
|
577
589
|
|
|
578
|
-
// If no audio files, use a timeout to emit "end" event
|
|
579
|
-
if (audioBuffers.length === 0) {
|
|
590
|
+
// If no audio files, use a timeout to emit "end" event
|
|
591
|
+
if (this.audioBuffers.length === 0) {
|
|
580
592
|
// Clear any existing timeout
|
|
581
593
|
if (this.endEventTimeout) {
|
|
582
594
|
clearTimeout(this.endEventTimeout);
|
|
@@ -591,12 +603,12 @@ class AudioManager {
|
|
|
591
603
|
}
|
|
592
604
|
}
|
|
593
605
|
|
|
594
|
-
for (const audioBuffer of audioBuffers) {
|
|
606
|
+
for (const audioBuffer of this.audioBuffers) {
|
|
595
607
|
const source = this.audioCtx.createBufferSource();
|
|
596
608
|
source.buffer = audioBuffer;
|
|
597
609
|
source.onended = () => {
|
|
598
610
|
endedCount++;
|
|
599
|
-
if (endedCount === audioBuffers.length) {
|
|
611
|
+
if (endedCount === this.audioBuffers.length && !this._isPaused) {
|
|
600
612
|
this.eventEmitter.emit("end");
|
|
601
613
|
}
|
|
602
614
|
};
|
|
@@ -614,19 +626,12 @@ class AudioManager {
|
|
|
614
626
|
this.audioSources.push(source);
|
|
615
627
|
}
|
|
616
628
|
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
617
|
-
this.pause();
|
|
618
629
|
}
|
|
619
630
|
|
|
620
631
|
/**
|
|
621
632
|
* @param percentComplete The progress between the start and end of the preview.
|
|
622
633
|
*/
|
|
623
634
|
async seek(percentComplete: number) {
|
|
624
|
-
// Suspend context first to prevent time drift during seek
|
|
625
|
-
// (chartCurrentTimeMs is calculated from audioCtx.currentTime)
|
|
626
|
-
if (this.audioCtx.state === "running") {
|
|
627
|
-
await this.audioCtx.suspend();
|
|
628
|
-
}
|
|
629
|
-
|
|
630
635
|
// Clear end event timeout when seeking
|
|
631
636
|
if (this.endEventTimeout) {
|
|
632
637
|
clearTimeout(this.endEventTimeout);
|
|
@@ -634,22 +639,12 @@ class AudioManager {
|
|
|
634
639
|
}
|
|
635
640
|
|
|
636
641
|
// Stop existing audio sources
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
source.onended = null;
|
|
640
|
-
try {
|
|
641
|
-
source.stop();
|
|
642
|
-
source.disconnect();
|
|
643
|
-
} catch {
|
|
644
|
-
// Source may already be stopped
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
this.audioSources = [];
|
|
648
|
-
this.gainNode?.disconnect();
|
|
649
|
-
this.gainNode = null;
|
|
642
|
+
this.stopAudioSources();
|
|
643
|
+
|
|
650
644
|
const chartSeekTimeMs = percentComplete * this.chartEndTimeMs;
|
|
651
645
|
this.lastSeekChartTimeMs = chartSeekTimeMs;
|
|
652
646
|
this.lastAudioCtxCurrentTime = this.audioCtx.currentTime;
|
|
647
|
+
this._isPaused = true;
|
|
653
648
|
}
|
|
654
649
|
}
|
|
655
650
|
|
|
@@ -57,7 +57,6 @@ const STYLES = `
|
|
|
57
57
|
--accent-color: #3b82f6;
|
|
58
58
|
--text-color: #e0e0e0;
|
|
59
59
|
--text-muted: #aaa;
|
|
60
|
-
--text-idle: #666;
|
|
61
60
|
--error-color: #ef4444;
|
|
62
61
|
--shadow-color: rgba(0, 0, 0, 0.3);
|
|
63
62
|
|
|
@@ -129,8 +128,8 @@ const STYLES = `
|
|
|
129
128
|
align-items: center;
|
|
130
129
|
justify-content: center;
|
|
131
130
|
height: 40px;
|
|
132
|
-
width:
|
|
133
|
-
padding:
|
|
131
|
+
width: 32px;
|
|
132
|
+
padding: 6px;
|
|
134
133
|
transition: background 0.15s;
|
|
135
134
|
flex-shrink: 0;
|
|
136
135
|
}
|
|
@@ -215,10 +214,10 @@ const STYLES = `
|
|
|
215
214
|
.timestamp {
|
|
216
215
|
color: var(--text-muted);
|
|
217
216
|
font-size: 12px;
|
|
218
|
-
padding: 0
|
|
217
|
+
padding: 0 2px;
|
|
219
218
|
white-space: nowrap;
|
|
220
219
|
flex-shrink: 0;
|
|
221
|
-
min-width:
|
|
220
|
+
min-width: 70px;
|
|
222
221
|
text-align: center;
|
|
223
222
|
}
|
|
224
223
|
|
|
@@ -226,7 +225,7 @@ const STYLES = `
|
|
|
226
225
|
flex: 1;
|
|
227
226
|
display: flex;
|
|
228
227
|
align-items: center;
|
|
229
|
-
padding: 0
|
|
228
|
+
padding: 0 4px;
|
|
230
229
|
min-width: 0;
|
|
231
230
|
}
|
|
232
231
|
|
|
@@ -268,11 +267,6 @@ const STYLES = `
|
|
|
268
267
|
cursor: not-allowed;
|
|
269
268
|
}
|
|
270
269
|
|
|
271
|
-
.idle-message {
|
|
272
|
-
color: var(--text-idle);
|
|
273
|
-
font-size: 14px;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
270
|
.error-message {
|
|
277
271
|
color: var(--error-color);
|
|
278
272
|
font-size: 14px;
|
|
@@ -379,10 +373,9 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
379
373
|
<style>${STYLES}</style>
|
|
380
374
|
<div class="player-container">
|
|
381
375
|
<div class="preview-viewport">
|
|
382
|
-
<span class="idle-message">Click play to start</span>
|
|
383
376
|
</div>
|
|
384
377
|
<div class="controls">
|
|
385
|
-
<button class="control-btn play-btn" aria-label="Play"
|
|
378
|
+
<button class="control-btn play-btn" aria-label="Play">
|
|
386
379
|
${ICONS.play}
|
|
387
380
|
</button>
|
|
388
381
|
<div class="volume-wrapper">
|
|
@@ -587,10 +580,6 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
587
580
|
private prepareForLoading(): void {
|
|
588
581
|
this.setState("loading");
|
|
589
582
|
|
|
590
|
-
// Remove idle message
|
|
591
|
-
const idleMessage = this.viewport.querySelector(".idle-message");
|
|
592
|
-
if (idleMessage) idleMessage.remove();
|
|
593
|
-
|
|
594
583
|
// Remove error message if present
|
|
595
584
|
const errorMessage = this.viewport.querySelector(".error-message");
|
|
596
585
|
if (errorMessage) errorMessage.remove();
|
|
@@ -644,7 +633,7 @@ export class ChartPreviewPlayer extends HTMLElement {
|
|
|
644
633
|
private updatePlayButton() {
|
|
645
634
|
switch (this._state) {
|
|
646
635
|
case "idle":
|
|
647
|
-
this.playBtn.disabled =
|
|
636
|
+
this.playBtn.disabled = false;
|
|
648
637
|
this.playBtn.innerHTML = ICONS.play;
|
|
649
638
|
this.playBtn.setAttribute("aria-label", "Play");
|
|
650
639
|
break;
|