@hustle-together/api-dev-tools 2.0.4 → 2.0.6
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/demo/audio/audio-sync.js +295 -0
- package/demo/audio/generate-narration.js +422 -0
- package/demo/audio/narration-timing.json +3614 -0
- package/demo/audio/narration-timing.sample.json +48 -0
- package/demo/audio/narration.mp3 +0 -0
- package/demo/workflow-demo.html +373 -25
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generated": "2025-12-08T00:00:00.000Z",
|
|
3
|
+
"duration": 300,
|
|
4
|
+
"wordCount": 850,
|
|
5
|
+
"sections": [
|
|
6
|
+
{ "id": "intro", "timestamp": 0 },
|
|
7
|
+
{ "id": "problems", "timestamp": 35 },
|
|
8
|
+
{ "id": "solution", "timestamp": 95 },
|
|
9
|
+
{ "id": "workflow", "timestamp": 140 },
|
|
10
|
+
{ "id": "installation", "timestamp": 220 },
|
|
11
|
+
{ "id": "credits", "timestamp": 260 },
|
|
12
|
+
{ "id": "outro", "timestamp": 285 }
|
|
13
|
+
],
|
|
14
|
+
"highlights": [
|
|
15
|
+
{ "selector": "#hustleBrand", "section": "intro", "timestamp": 2 },
|
|
16
|
+
{ "selector": "[data-phase='research']", "section": "intro", "timestamp": 8 },
|
|
17
|
+
{ "selector": "[data-phase='interview']", "section": "intro", "timestamp": 14 },
|
|
18
|
+
{ "selector": "[data-phase='test']", "section": "intro", "timestamp": 20 },
|
|
19
|
+
{ "selector": "[data-phase='code']", "section": "intro", "timestamp": 26 },
|
|
20
|
+
{ "selector": "[data-phase='docs']", "section": "intro", "timestamp": 32 },
|
|
21
|
+
{ "selector": "#problems h2", "section": "problems", "timestamp": 36 },
|
|
22
|
+
{ "selector": ".gap-item:nth-child(1)", "section": "problems", "timestamp": 42 },
|
|
23
|
+
{ "selector": ".gap-item:nth-child(2)", "section": "problems", "timestamp": 54 },
|
|
24
|
+
{ "selector": ".gap-item:nth-child(3)", "section": "problems", "timestamp": 66 },
|
|
25
|
+
{ "selector": ".gap-item:nth-child(4)", "section": "problems", "timestamp": 78 },
|
|
26
|
+
{ "selector": ".gap-item:nth-child(5)", "section": "problems", "timestamp": 88 },
|
|
27
|
+
{ "selector": "#solution h2", "section": "solution", "timestamp": 96 },
|
|
28
|
+
{ "selector": ".hook-box:nth-child(1)", "section": "solution", "timestamp": 104 },
|
|
29
|
+
{ "selector": ".hook-box:nth-child(2)", "section": "solution", "timestamp": 118 },
|
|
30
|
+
{ "selector": ".hook-box:nth-child(3)", "section": "solution", "timestamp": 130 },
|
|
31
|
+
{ "selector": "#workflow h2", "section": "workflow", "timestamp": 141 },
|
|
32
|
+
{ "selector": ".workflow-phase:nth-child(1)", "section": "workflow", "timestamp": 148 },
|
|
33
|
+
{ "selector": ".workflow-phase:nth-child(2)", "section": "workflow", "timestamp": 155 },
|
|
34
|
+
{ "selector": ".workflow-phase:nth-child(3)", "section": "workflow", "timestamp": 162 },
|
|
35
|
+
{ "selector": ".workflow-phase:nth-child(4)", "section": "workflow", "timestamp": 172 },
|
|
36
|
+
{ "selector": ".workflow-phase:nth-child(5)", "section": "workflow", "timestamp": 180 },
|
|
37
|
+
{ "selector": ".workflow-phase:nth-child(6)", "section": "workflow", "timestamp": 188 },
|
|
38
|
+
{ "selector": ".workflow-phase:nth-child(7)", "section": "workflow", "timestamp": 196 },
|
|
39
|
+
{ "selector": ".workflow-phase:nth-child(8)", "section": "workflow", "timestamp": 204 },
|
|
40
|
+
{ "selector": ".workflow-phase:nth-child(9)", "section": "workflow", "timestamp": 211 },
|
|
41
|
+
{ "selector": ".workflow-phase:nth-child(10)", "section": "workflow", "timestamp": 218 },
|
|
42
|
+
{ "selector": "#installation h2", "section": "installation", "timestamp": 221 },
|
|
43
|
+
{ "selector": ".install-command", "section": "installation", "timestamp": 228 },
|
|
44
|
+
{ "selector": "#credits h2", "section": "credits", "timestamp": 261 },
|
|
45
|
+
{ "selector": "#intro", "section": "outro", "timestamp": 286 }
|
|
46
|
+
],
|
|
47
|
+
"words": []
|
|
48
|
+
}
|
|
Binary file
|
package/demo/workflow-demo.html
CHANGED
|
@@ -500,6 +500,95 @@
|
|
|
500
500
|
box-shadow: 0 0 15px var(--accent-red-glow);
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
.nav-btn.active {
|
|
504
|
+
background: var(--accent-red);
|
|
505
|
+
color: #fff;
|
|
506
|
+
border-color: var(--accent-red);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.nav-btn.disabled {
|
|
510
|
+
opacity: 0.5;
|
|
511
|
+
cursor: not-allowed;
|
|
512
|
+
text-decoration: line-through;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.nav-btn.disabled:hover {
|
|
516
|
+
background: transparent;
|
|
517
|
+
color: var(--grey);
|
|
518
|
+
border-color: var(--grey);
|
|
519
|
+
box-shadow: none;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/* ============================================
|
|
523
|
+
AUDIO NARRATION PLAYER
|
|
524
|
+
============================================ */
|
|
525
|
+
.audio-progress-container {
|
|
526
|
+
position: fixed;
|
|
527
|
+
bottom: 60px;
|
|
528
|
+
left: 50%;
|
|
529
|
+
transform: translateX(-50%);
|
|
530
|
+
width: 80%;
|
|
531
|
+
max-width: 600px;
|
|
532
|
+
background: rgba(0, 0, 0, 0.9);
|
|
533
|
+
border: 1px dashed var(--grey);
|
|
534
|
+
padding: 15px 20px;
|
|
535
|
+
z-index: 1001;
|
|
536
|
+
border-radius: 0;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.audio-time-display {
|
|
540
|
+
display: flex;
|
|
541
|
+
justify-content: space-between;
|
|
542
|
+
font-size: 0.75rem;
|
|
543
|
+
color: var(--grey);
|
|
544
|
+
margin-bottom: 10px;
|
|
545
|
+
font-family: 'Courier New', monospace;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.audio-progress-bar {
|
|
549
|
+
position: relative;
|
|
550
|
+
height: 6px;
|
|
551
|
+
background: var(--dark-grey);
|
|
552
|
+
cursor: pointer;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.audio-progress-fill {
|
|
556
|
+
height: 100%;
|
|
557
|
+
background: var(--accent-red);
|
|
558
|
+
width: 0%;
|
|
559
|
+
transition: width 0.1s linear;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.audio-section-markers {
|
|
563
|
+
position: absolute;
|
|
564
|
+
top: 0;
|
|
565
|
+
left: 0;
|
|
566
|
+
width: 100%;
|
|
567
|
+
height: 100%;
|
|
568
|
+
pointer-events: none;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.audio-section-marker {
|
|
572
|
+
position: absolute;
|
|
573
|
+
top: -3px;
|
|
574
|
+
width: 2px;
|
|
575
|
+
height: 12px;
|
|
576
|
+
background: var(--white);
|
|
577
|
+
opacity: 0.5;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.audio-section-marker:hover {
|
|
581
|
+
opacity: 1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* Audio highlight effect for elements being narrated */
|
|
585
|
+
.audio-highlighted {
|
|
586
|
+
box-shadow: 0 0 30px var(--accent-red-glow), 0 0 60px var(--accent-red-glow) !important;
|
|
587
|
+
border-color: var(--accent-red) !important;
|
|
588
|
+
transform: scale(1.02);
|
|
589
|
+
transition: all 0.3s ease !important;
|
|
590
|
+
}
|
|
591
|
+
|
|
503
592
|
/* Progress indicator */
|
|
504
593
|
.progress-bar {
|
|
505
594
|
position: fixed;
|
|
@@ -1589,10 +1678,28 @@
|
|
|
1589
1678
|
|
|
1590
1679
|
<!-- Navigation -->
|
|
1591
1680
|
<nav class="nav">
|
|
1592
|
-
<button class="nav-btn" id="playBtn"
|
|
1593
|
-
<button class="nav-btn" id="
|
|
1681
|
+
<button class="nav-btn" id="playBtn">▶ AUTO PLAY</button>
|
|
1682
|
+
<button class="nav-btn" id="audioToggleBtn" title="Toggle narration audio">🔊 WITH AUDIO</button>
|
|
1683
|
+
<button class="nav-btn" id="resetBtn">↺ RESTART</button>
|
|
1594
1684
|
</nav>
|
|
1595
1685
|
|
|
1686
|
+
<!-- Audio Narration Player (hidden) -->
|
|
1687
|
+
<audio id="narrationAudio" preload="auto">
|
|
1688
|
+
<source src="audio/narration.mp3" type="audio/mpeg">
|
|
1689
|
+
</audio>
|
|
1690
|
+
|
|
1691
|
+
<!-- Audio Progress Bar -->
|
|
1692
|
+
<div class="audio-progress-container" id="audioProgressContainer" style="display: none;">
|
|
1693
|
+
<div class="audio-time-display">
|
|
1694
|
+
<span id="audioCurrentTime">0:00</span>
|
|
1695
|
+
<span id="audioTotalTime">0:00</span>
|
|
1696
|
+
</div>
|
|
1697
|
+
<div class="audio-progress-bar" id="audioProgressBar">
|
|
1698
|
+
<div class="audio-progress-fill" id="audioProgressFill"></div>
|
|
1699
|
+
<div class="audio-section-markers" id="audioSectionMarkers"></div>
|
|
1700
|
+
</div>
|
|
1701
|
+
</div>
|
|
1702
|
+
|
|
1596
1703
|
<!-- Section Indicators -->
|
|
1597
1704
|
<div class="section-indicator" id="sectionIndicator"></div>
|
|
1598
1705
|
|
|
@@ -1606,7 +1713,7 @@
|
|
|
1606
1713
|
<span class="hustle-word hustle-highlight">API-DEV-TOOLS</span>
|
|
1607
1714
|
</div>
|
|
1608
1715
|
<div class="package-name" id="packageName">@hustle-together/api-dev-tools</div>
|
|
1609
|
-
<div class="version" id="versionText">v2.0.
|
|
1716
|
+
<div class="version" id="versionText">v2.0.6</div>
|
|
1610
1717
|
<p class="tagline">"Hustle together. Share resources. Build stronger."<span class="cursor"></span></p>
|
|
1611
1718
|
|
|
1612
1719
|
<div class="intro-text">
|
|
@@ -2664,32 +2771,272 @@
|
|
|
2664
2771
|
.to('.made-with', { opacity: 1, duration: 0.5 });
|
|
2665
2772
|
|
|
2666
2773
|
// ============================================
|
|
2667
|
-
// AUTO-PLAY
|
|
2774
|
+
// UNIFIED AUTO-PLAY + AUDIO SYSTEM
|
|
2668
2775
|
// ============================================
|
|
2669
|
-
document.getElementById('playBtn')
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2776
|
+
const playBtn = document.getElementById('playBtn');
|
|
2777
|
+
const audioToggleBtn = document.getElementById('audioToggleBtn');
|
|
2778
|
+
const narrationAudio = document.getElementById('narrationAudio');
|
|
2779
|
+
const audioProgressContainer = document.getElementById('audioProgressContainer');
|
|
2780
|
+
const audioProgressBar = document.getElementById('audioProgressBar');
|
|
2781
|
+
const audioProgressFill = document.getElementById('audioProgressFill');
|
|
2782
|
+
const audioCurrentTime = document.getElementById('audioCurrentTime');
|
|
2783
|
+
const audioTotalTime = document.getElementById('audioTotalTime');
|
|
2784
|
+
const audioSectionMarkers = document.getElementById('audioSectionMarkers');
|
|
2785
|
+
|
|
2786
|
+
let audioEnabled = true; // Audio on by default
|
|
2787
|
+
let timingData = null;
|
|
2788
|
+
let currentHighlight = null;
|
|
2789
|
+
let highlightIndex = 0;
|
|
2790
|
+
let currentSectionIdx = 0;
|
|
2791
|
+
let autoPlayInterval = null;
|
|
2792
|
+
|
|
2793
|
+
// Format seconds to MM:SS
|
|
2794
|
+
function formatTime(seconds) {
|
|
2795
|
+
const mins = Math.floor(seconds / 60);
|
|
2796
|
+
const secs = Math.floor(seconds % 60);
|
|
2797
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// Load timing data
|
|
2801
|
+
async function loadTimingData() {
|
|
2802
|
+
if (timingData) return timingData;
|
|
2803
|
+
try {
|
|
2804
|
+
const response = await fetch('audio/narration-timing.json');
|
|
2805
|
+
if (!response.ok) throw new Error('Failed to load timing data');
|
|
2806
|
+
timingData = await response.json();
|
|
2807
|
+
|
|
2808
|
+
// Update total time display
|
|
2809
|
+
if (audioTotalTime) {
|
|
2810
|
+
audioTotalTime.textContent = formatTime(timingData.duration);
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// Create section markers on progress bar
|
|
2814
|
+
if (audioSectionMarkers && timingData.sections) {
|
|
2815
|
+
audioSectionMarkers.innerHTML = '';
|
|
2816
|
+
timingData.sections.forEach((section, i) => {
|
|
2817
|
+
if (i === 0) return;
|
|
2818
|
+
const marker = document.createElement('div');
|
|
2819
|
+
marker.className = 'audio-section-marker';
|
|
2820
|
+
marker.style.left = `${(section.timestamp / timingData.duration) * 100}%`;
|
|
2821
|
+
marker.title = section.id.toUpperCase();
|
|
2822
|
+
audioSectionMarkers.appendChild(marker);
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
console.log('Timing data loaded:', timingData.sections.length, 'sections,', timingData.highlights.length, 'highlights');
|
|
2827
|
+
return timingData;
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
console.error('Failed to load timing data:', error);
|
|
2830
|
+
return null;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// Apply highlight to an element
|
|
2835
|
+
function applyHighlight(selector) {
|
|
2836
|
+
removeHighlight();
|
|
2837
|
+
try {
|
|
2838
|
+
const el = document.querySelector(selector);
|
|
2839
|
+
if (el) {
|
|
2840
|
+
currentHighlight = el;
|
|
2841
|
+
el.classList.add('audio-highlighted');
|
|
2842
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
2843
|
+
gsap.fromTo(el,
|
|
2844
|
+
{ boxShadow: '0 0 0px var(--accent-red-glow)' },
|
|
2845
|
+
{ boxShadow: '0 0 40px var(--accent-red-glow), 0 0 80px var(--accent-red-glow)', duration: 0.4, ease: 'power2.out' }
|
|
2846
|
+
);
|
|
2847
|
+
}
|
|
2848
|
+
} catch (e) {
|
|
2849
|
+
console.warn('Could not highlight:', selector, e);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// Remove current highlight
|
|
2854
|
+
function removeHighlight() {
|
|
2855
|
+
if (currentHighlight) {
|
|
2856
|
+
currentHighlight.classList.remove('audio-highlighted');
|
|
2857
|
+
gsap.to(currentHighlight, { boxShadow: 'none', duration: 0.3 });
|
|
2858
|
+
currentHighlight = null;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// Handle audio time update (syncs highlights with narration)
|
|
2863
|
+
function onAudioTimeUpdate() {
|
|
2864
|
+
if (!timingData || !audioEnabled) return;
|
|
2865
|
+
|
|
2866
|
+
const currentTime = narrationAudio.currentTime;
|
|
2867
|
+
|
|
2868
|
+
// Update progress bar
|
|
2869
|
+
const progress = (currentTime / timingData.duration) * 100;
|
|
2870
|
+
audioProgressFill.style.width = `${progress}%`;
|
|
2871
|
+
|
|
2872
|
+
// Update time display
|
|
2873
|
+
if (audioCurrentTime) {
|
|
2874
|
+
audioCurrentTime.textContent = formatTime(currentTime);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// Check for highlight triggers
|
|
2878
|
+
for (let i = highlightIndex; i < timingData.highlights.length; i++) {
|
|
2879
|
+
const highlight = timingData.highlights[i];
|
|
2880
|
+
if (currentTime >= highlight.timestamp) {
|
|
2881
|
+
if (i !== highlightIndex || !currentHighlight) {
|
|
2882
|
+
highlightIndex = i;
|
|
2883
|
+
applyHighlight(highlight.selector);
|
|
2689
2884
|
}
|
|
2690
|
-
}
|
|
2885
|
+
} else {
|
|
2886
|
+
break;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
// Handle seek on progress bar click
|
|
2892
|
+
function onProgressBarClick(e) {
|
|
2893
|
+
if (!timingData || !narrationAudio.duration) return;
|
|
2894
|
+
|
|
2895
|
+
const rect = audioProgressBar.getBoundingClientRect();
|
|
2896
|
+
const clickX = e.clientX - rect.left;
|
|
2897
|
+
const percentage = clickX / rect.width;
|
|
2898
|
+
const seekTime = percentage * timingData.duration;
|
|
2899
|
+
|
|
2900
|
+
narrationAudio.currentTime = seekTime;
|
|
2901
|
+
|
|
2902
|
+
// Reset highlight tracking
|
|
2903
|
+
highlightIndex = 0;
|
|
2904
|
+
for (let i = 0; i < timingData.highlights.length; i++) {
|
|
2905
|
+
if (timingData.highlights[i].timestamp <= seekTime) {
|
|
2906
|
+
highlightIndex = i;
|
|
2907
|
+
} else {
|
|
2908
|
+
break;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// Silent auto-play (no audio, timed sections)
|
|
2914
|
+
function startSilentAutoPlay() {
|
|
2915
|
+
let sectionIdx = 0;
|
|
2916
|
+
autoPlayInterval = setInterval(() => {
|
|
2917
|
+
if (sectionIdx < sections.length) {
|
|
2918
|
+
document.getElementById(sections[sectionIdx]).scrollIntoView({ behavior: 'smooth' });
|
|
2919
|
+
sectionIdx++;
|
|
2920
|
+
} else {
|
|
2921
|
+
stopAutoPlay();
|
|
2922
|
+
}
|
|
2923
|
+
}, 5000);
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// Start auto-play
|
|
2927
|
+
async function startAutoPlay() {
|
|
2928
|
+
if (isPlaying) return;
|
|
2929
|
+
|
|
2930
|
+
isPlaying = true;
|
|
2931
|
+
playBtn.classList.add('active');
|
|
2932
|
+
playBtn.textContent = '⏸ STOP';
|
|
2933
|
+
|
|
2934
|
+
// Scroll to top first
|
|
2935
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
2936
|
+
|
|
2937
|
+
await loadTimingData();
|
|
2938
|
+
|
|
2939
|
+
if (audioEnabled && timingData) {
|
|
2940
|
+
// Try to play audio
|
|
2941
|
+
audioProgressContainer.style.display = 'block';
|
|
2942
|
+
|
|
2943
|
+
try {
|
|
2944
|
+
await narrationAudio.play();
|
|
2945
|
+
console.log('Audio playing');
|
|
2946
|
+
} catch (err) {
|
|
2947
|
+
console.error('Audio playback failed:', err);
|
|
2948
|
+
// Fall back to silent mode
|
|
2949
|
+
audioEnabled = false;
|
|
2950
|
+
audioToggleBtn.textContent = '🔇 NO AUDIO';
|
|
2951
|
+
audioToggleBtn.classList.add('disabled');
|
|
2952
|
+
audioProgressContainer.style.display = 'none';
|
|
2953
|
+
startSilentAutoPlay();
|
|
2954
|
+
}
|
|
2955
|
+
} else {
|
|
2956
|
+
// Silent auto-play
|
|
2957
|
+
startSilentAutoPlay();
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// Stop auto-play
|
|
2962
|
+
function stopAutoPlay() {
|
|
2963
|
+
isPlaying = false;
|
|
2964
|
+
playBtn.classList.remove('active');
|
|
2965
|
+
playBtn.textContent = '▶ AUTO PLAY';
|
|
2966
|
+
|
|
2967
|
+
if (autoPlayInterval) {
|
|
2968
|
+
clearInterval(autoPlayInterval);
|
|
2969
|
+
autoPlayInterval = null;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
narrationAudio.pause();
|
|
2973
|
+
narrationAudio.currentTime = 0;
|
|
2974
|
+
audioProgressContainer.style.display = 'none';
|
|
2975
|
+
removeHighlight();
|
|
2976
|
+
highlightIndex = 0;
|
|
2977
|
+
currentSectionIdx = 0;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// Audio ended
|
|
2981
|
+
function onAudioEnded() {
|
|
2982
|
+
stopAutoPlay();
|
|
2983
|
+
setTimeout(() => {
|
|
2984
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
2985
|
+
}, 500);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// Toggle audio on/off
|
|
2989
|
+
function toggleAudio() {
|
|
2990
|
+
audioEnabled = !audioEnabled;
|
|
2991
|
+
if (audioEnabled) {
|
|
2992
|
+
audioToggleBtn.textContent = '🔊 WITH AUDIO';
|
|
2993
|
+
audioToggleBtn.classList.remove('disabled');
|
|
2994
|
+
} else {
|
|
2995
|
+
audioToggleBtn.textContent = '🔇 SILENT';
|
|
2996
|
+
audioToggleBtn.classList.add('disabled');
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// If currently playing, adjust
|
|
3000
|
+
if (isPlaying) {
|
|
3001
|
+
if (audioEnabled) {
|
|
3002
|
+
narrationAudio.play().catch(() => {});
|
|
3003
|
+
audioProgressContainer.style.display = 'block';
|
|
3004
|
+
if (autoPlayInterval) {
|
|
3005
|
+
clearInterval(autoPlayInterval);
|
|
3006
|
+
autoPlayInterval = null;
|
|
3007
|
+
}
|
|
3008
|
+
} else {
|
|
3009
|
+
narrationAudio.pause();
|
|
3010
|
+
audioProgressContainer.style.display = 'none';
|
|
3011
|
+
startSilentAutoPlay();
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
// Event listeners
|
|
3017
|
+
playBtn.addEventListener('click', () => {
|
|
3018
|
+
if (isPlaying) {
|
|
3019
|
+
stopAutoPlay();
|
|
3020
|
+
} else {
|
|
3021
|
+
startAutoPlay();
|
|
2691
3022
|
}
|
|
2692
3023
|
});
|
|
3024
|
+
audioToggleBtn.addEventListener('click', toggleAudio);
|
|
3025
|
+
narrationAudio.addEventListener('timeupdate', onAudioTimeUpdate);
|
|
3026
|
+
narrationAudio.addEventListener('ended', onAudioEnded);
|
|
3027
|
+
audioProgressBar.addEventListener('click', onProgressBarClick);
|
|
3028
|
+
|
|
3029
|
+
// Check if audio file is accessible
|
|
3030
|
+
narrationAudio.addEventListener('error', (e) => {
|
|
3031
|
+
console.error('Audio error:', e);
|
|
3032
|
+
audioEnabled = false;
|
|
3033
|
+
audioToggleBtn.textContent = '🔇 NO AUDIO';
|
|
3034
|
+
audioToggleBtn.classList.add('disabled');
|
|
3035
|
+
audioToggleBtn.title = 'Audio file not accessible (run from a web server)';
|
|
3036
|
+
});
|
|
3037
|
+
|
|
3038
|
+
// Preload timing data
|
|
3039
|
+
loadTimingData();
|
|
2693
3040
|
|
|
2694
3041
|
// ============================================
|
|
2695
3042
|
// RESET BUTTON
|
|
@@ -2889,6 +3236,7 @@
|
|
|
2889
3236
|
}
|
|
2890
3237
|
});
|
|
2891
3238
|
});
|
|
3239
|
+
|
|
2892
3240
|
</script>
|
|
2893
3241
|
|
|
2894
3242
|
</body>
|
package/package.json
CHANGED