@@ -30,12 +30,16 @@ const VERSE_READ_DELAY = 2200;
3030 let autoReadingFontSizeKey = 'normal' ;
3131 let autoReadingFontSizeValue = 1.5 ;
3232 let autoReadingActive = false ;
33+ let speechReadingActive = false ;
34+ let speechRefIndex = 0 ;
35+ let speechUtterance = null ;
36+ const speechSupported = typeof window . speechSynthesis !== 'undefined' && typeof window . SpeechSynthesisUtterance !== 'undefined' ;
3337 const AUTO_READING_SPEEDS = {
34- 'muy-lento' : { label : 'Muy lento' , value : 4500 } ,
35- 'lento' : { label : 'Lento' , value : 3200 } ,
36- 'normal' : { label : 'Normal' , value : 2200 } ,
37- 'rapido' : { label : 'Rápido' , value : 1500 } ,
38- 'muy-rapido' : { label : 'Muy rápido' , value : 900 }
38+ 'muy-lento' : { label : 'Muy lento' , value : 4500 , speechRate : 0.75 } ,
39+ 'lento' : { label : 'Lento' , value : 3200 , speechRate : 0.9 } ,
40+ 'normal' : { label : 'Normal' , value : 2200 , speechRate : 1 } ,
41+ 'rapido' : { label : 'Rápido' , value : 1500 , speechRate : 1.2 } ,
42+ 'muy-rapido' : { label : 'Muy rápido' , value : 900 , speechRate : 1.35 }
3943 } ;
4044 const AUTO_READING_FONT_SIZES = {
4145 'normal' : { label : 'Normal' , value : 1 } ,
@@ -49,13 +53,20 @@ const VERSE_READ_DELAY = 2200;
4953 const toggle = trackerBar ? trackerBar . querySelector ( '#btn-reading-toggle' ) : null ;
5054 const speed = trackerBar ? trackerBar . querySelector ( '#reading-speed-select' ) : null ;
5155 const fontSize = trackerBar ? trackerBar . querySelector ( '#reading-font-size-select' ) : null ;
56+ const floatingBtn = document . getElementById ( 'floating-read-btn' ) ;
5257 if ( toggle ) {
5358 toggle . textContent = autoReadingActive ? '⏸ Detener' : '▶ Leer' ;
5459 toggle . title = autoReadingActive ? 'Pausar lectura automática' : 'Iniciar lectura automática' ;
5560 toggle . style . background = autoReadingActive ? '#ff7043' : 'var(--secondary)' ;
5661 }
5762 if ( speed ) speed . value = autoReadingSpeedKey ;
5863 if ( fontSize ) fontSize . value = autoReadingFontSizeKey ;
64+ if ( floatingBtn ) {
65+ floatingBtn . textContent = autoReadingActive ? '⏸' : '▶' ;
66+ floatingBtn . classList . toggle ( 'active' , autoReadingActive ) ;
67+ const showFloating = window . innerWidth <= 768 && trackerBar && trackerBar . style . display !== 'none' ;
68+ floatingBtn . style . display = showFloating ? 'flex' : 'none' ;
69+ }
5970 } ;
6071
6172 window . setReadingAutoSpeed = ( key ) => {
@@ -115,6 +126,7 @@ const VERSE_READ_DELAY = 2200;
115126
116127 window . startReadingAutoPlay = ( ) => {
117128 if ( autoReadingActive ) return ;
129+ if ( speechReadingActive ) window . stopSpeechRead ( ) ;
118130 autoReadingActive = true ;
119131 window . updateReadingAutoControls ( ) ;
120132 window . advanceReadingAutoPlay ( ) ;
@@ -125,6 +137,77 @@ const VERSE_READ_DELAY = 2200;
125137 else window . startReadingAutoPlay ( ) ;
126138 } ;
127139
140+ const getVisibleVerseSpans = ( ) => Array . from ( document . querySelectorAll ( '.verse-text-span' ) ) . filter ( span => span . offsetParent !== null ) ;
141+ const getCurrentSpeechIndex = ( ) => {
142+ const spans = getVisibleVerseSpans ( ) ;
143+ const active = document . querySelector ( '.verse-text-span.active-reading' ) ;
144+ if ( active ) {
145+ const idx = spans . indexOf ( active ) ;
146+ return idx >= 0 ? idx : 0 ;
147+ }
148+ return 0 ;
149+ } ;
150+
151+ const speakSpanAtIndex = ( index ) => {
152+ const spans = getVisibleVerseSpans ( ) ;
153+ if ( ! speechSupported || index < 0 || index >= spans . length ) {
154+ window . stopSpeechRead ( ) ;
155+ return ;
156+ }
157+ speechRefIndex = index ;
158+ const span = spans [ speechRefIndex ] ;
159+ const textContent = span . textContent || span . innerText || '' ;
160+ const text = `${ span . dataset . ref || '' } . ${ textContent . replace ( / \s + / g, ' ' ) . trim ( ) } ` ;
161+ if ( speechUtterance ) {
162+ window . speechSynthesis . cancel ( ) ;
163+ }
164+ speechUtterance = new SpeechSynthesisUtterance ( text ) ;
165+ speechUtterance . rate = AUTO_READING_SPEEDS [ autoReadingSpeedKey ] ?. speechRate || 1 ;
166+ speechUtterance . onend = ( ) => {
167+ const nextIndex = speechRefIndex + 1 ;
168+ const nextSpans = getVisibleVerseSpans ( ) ;
169+ if ( speechReadingActive && nextIndex < nextSpans . length ) {
170+ speakSpanAtIndex ( nextIndex ) ;
171+ } else {
172+ window . stopSpeechRead ( ) ;
173+ }
174+ } ;
175+ speechUtterance . onerror = ( ) => {
176+ window . stopSpeechRead ( ) ;
177+ } ;
178+ window . speechSynthesis . speak ( speechUtterance ) ;
179+ if ( span ) {
180+ document . querySelectorAll ( '.verse-text-span.selected-verse' ) . forEach ( el => el . classList . remove ( 'selected-verse' ) ) ;
181+ span . classList . add ( 'selected-verse' ) ;
182+ span . scrollIntoView ( { behavior : 'smooth' , block : 'center' } ) ;
183+ }
184+ } ;
185+
186+ window . startSpeechRead = ( ) => {
187+ if ( ! speechSupported || speechReadingActive ) return ;
188+ if ( autoReadingActive ) window . stopReadingAutoPlay ( ) ;
189+ speechReadingActive = true ;
190+ speechRefIndex = getCurrentSpeechIndex ( ) ;
191+ window . updateReadingAutoControls ( ) ;
192+ speakSpanAtIndex ( speechRefIndex ) ;
193+ } ;
194+
195+ window . stopSpeechRead = ( ) => {
196+ if ( ! speechSupported ) return ;
197+ speechReadingActive = false ;
198+ if ( speechUtterance ) {
199+ window . speechSynthesis . cancel ( ) ;
200+ speechUtterance = null ;
201+ }
202+ window . updateReadingAutoControls ( ) ;
203+ } ;
204+
205+ window . toggleSpeechRead = ( ) => {
206+ if ( ! speechSupported ) return ;
207+ if ( speechReadingActive ) window . stopSpeechRead ( ) ;
208+ else window . startSpeechRead ( ) ;
209+ } ;
210+
128211 const pushHistory = ( fn , params ) => {
129212 if ( historyStack . length > 0 ) {
130213 let last = historyStack [ historyStack . length - 1 ] ;
@@ -184,8 +267,19 @@ const VERSE_READ_DELAY = 2200;
184267 const SHORT_HOLD = 300 ; // 300ms para mostrar la burbuja amarilla
185268 const DOUBLE_TAP = 300 ; // Tiempo máximo entre dos toques rápidos
186269
270+ let isActionButtonTouch = false ;
271+ const getActionButton = ( node ) => {
272+ if ( ! node || ! node . closest ) return null ;
273+ return node . closest ( 'button.add-to-session-btn, button.add-to-clipboard-btn' ) ;
274+ } ;
275+
187276 // A. El dedo TOCA la pantalla
188277 span . addEventListener ( 'touchstart' , ( e ) => {
278+ const actionButton = getActionButton ( e . target ) || getActionButton ( e . target . parentNode ) ;
279+ if ( actionButton ) {
280+ isActionButtonTouch = true ;
281+ return ;
282+ }
189283 touchStartTime = new Date ( ) . getTime ( ) ;
190284 longPressTriggered = false ;
191285 // Iniciamos el cronómetro para la burbuja amarilla
@@ -200,14 +294,19 @@ const VERSE_READ_DELAY = 2200;
200294 span . addEventListener ( 'touchmove' , ( ) => {
201295 clearTimeout ( touchTimer ) ;
202296 span . classList . remove ( 'force-yellow' ) ;
297+ if ( isActionButtonTouch ) return ;
203298 if ( currentTooltipTarget === span ) {
204- window . _hideFloatTip ( ) ;
299+ window . _hideFloatTip ( true ) ;
205300 currentTooltipTarget = null ;
206301 }
207302 } , { passive : true } ) ;
208303
209304 // C. El dedo SUELTA la pantalla
210305 span . addEventListener ( 'touchend' , ( e ) => {
306+ if ( isActionButtonTouch ) {
307+ isActionButtonTouch = false ;
308+ return ;
309+ }
211310 clearTimeout ( touchTimer ) ; // Evitar burbujas accidentales si soltó rápido
212311 span . classList . remove ( 'force-yellow' ) ;
213312
@@ -223,16 +322,16 @@ const VERSE_READ_DELAY = 2200;
223322 // ¿Fue un Doble Toque Rápido?
224323 if ( tapLength < DOUBLE_TAP && tapLength > 0 ) {
225324 e . preventDefault ( ) ;
226- window . _hideFloatTip ( ) ;
325+ window . _hideFloatTip ( true ) ;
227326 currentTooltipTarget = null ;
228327 viewSingleVerse ( verseId ) ; // Entra a las notas del versículo
229328 } else if ( touchDuration < SHORT_HOLD && span . dataset . tt ) {
230329 // Tap corto: alterna tooltip
231330 if ( currentTooltipTarget === span ) {
232- window . _hideFloatTip ( ) ;
331+ window . _hideFloatTip ( true ) ;
233332 currentTooltipTarget = null ;
234333 } else {
235- window . _hideFloatTip ( ) ;
334+ window . _hideFloatTip ( true ) ;
236335 currentTooltipTarget = span ;
237336 window . _showFloatTip ( { currentTarget : span , clientX : touchPoint . clientX , clientY : touchPoint . clientY } ) ;
238337 }
@@ -531,7 +630,7 @@ const VERSE_READ_DELAY = 2200;
531630 const trackerBar = document . getElementById ( 'sticky-tracker-bar' ) ;
532631 if ( trackerBar ) {
533632 trackerBar . style . display = 'flex' ;
534- trackerBar . innerHTML = `<div id="tracker-ref" class="tracker-info" style="flex:1;">${ title } </div><div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;"><button id="btn-reading-toggle" title="Iniciar lectura automática" style="background:var(--secondary);border:none;border-radius:6px;padding:6px 12px;color:white;font-weight:bold;cursor:pointer;flex-shrink:0;">▶ Leer</button><label style="display:flex;align-items:center;gap:6px;color:white;font-size:0.9rem;margin:0;">Velocidad:<select id="reading-speed-select" style="border-radius:6px;padding:4px 8px;border:none;font-size:0.95rem;color:#333;"> <option value="muy-lento">Muy lento</option> <option value="lento">Lento</option> <option value="normal" selected>Normal</option> <option value="rapido">Rápido</option> <option value="muy-rapido">Muy rápido</option> </select></label><label style="display:flex;align-items:center;gap:6px;color:white;font-size:0.9rem;margin:0;">Tamaño:<select id="reading-font-size-select" style="border-radius:6px;padding:4px 8px;border:none;font-size:0.95rem;color:#333;"> <option value="normal">Normal</option> <option value="x1.5">1.5×</option> <option value="x2">2×</option> <option value="x3">3×</option> </select></label></div><button id="btn-range-toggle" title="Filtrar por Rango" style="background:var(--secondary);border:none;border-radius:6px;padding:4px 10px;color:white;font-weight:bold;cursor:pointer;font-size:0.9rem;flex-shrink:0;">⊟ Rango</button>` ;
633+ trackerBar . innerHTML = `<div id="tracker-ref" class="tracker-info" style="flex:1;">${ title } </div><div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;"><button id="btn-reading-toggle" title="Iniciar lectura automática" style="background:var(--secondary);border:none;border-radius:6px;padding:6px 12px;color:white;font-weight:bold;cursor:pointer;flex-shrink:0;">▶ Leer</button><button id="btn-speech-toggle" title="Leer en voz alta" style="background:var(--secondary);border:none;border-radius:6px;padding:6px 12px;color:white;font-weight:bold;cursor:pointer;flex-shrink:0;">🔊 Voz</button><label style="display:flex;align-items:center;gap:6px;color:white;font-size:0.9rem;margin:0;">Velocidad:<select id="reading-speed-select" style="border-radius:6px;padding:4px 8px;border:none;font-size:0.95rem;color:#333;"> <option value="muy-lento">Muy lento</option> <option value="lento">Lento</option> <option value="normal" selected>Normal</option> <option value="rapido">Rápido</option> <option value="muy-rapido">Muy rápido</option> </select></label><label style="display:flex;align-items:center;gap:6px;color:white;font-size:0.9rem;margin:0;">Tamaño:<select id="reading-font-size-select" style="border-radius:6px;padding:4px 8px;border:none;font-size:0.95rem;color:#333;"> <option value="normal">Normal</option> <option value="x1.5">1.5×</option> <option value="x2">2×</option> <option value="x3">3×</option> </select></label></div><button id="btn-range-toggle" title="Filtrar por Rango" style="background:var(--secondary);border:none;border-radius:6px;padding:4px 10px;color:white;font-weight:bold;cursor:pointer;font-size:0.9rem;flex-shrink:0;">⊟ Rango</button>` ;
535634 window . updateReadingAutoControls ( ) ;
536635 window . setReadingAutoFontSize ( autoReadingFontSizeKey ) ;
537636 }
@@ -556,6 +655,11 @@ const VERSE_READ_DELAY = 2200;
556655 window . toggleReadingAutoPlay ( ) ;
557656 return ;
558657 }
658+ const speechBtn = ev . target . closest ( '#btn-speech-toggle' ) ;
659+ if ( speechBtn ) {
660+ window . toggleSpeechRead ( ) ;
661+ return ;
662+ }
559663 const btn = ev . target . closest ( '#btn-range-toggle' ) ;
560664 if ( ! btn ) return ;
561665 const rb = document . getElementById ( 'range-selector-bar' ) ;
0 commit comments