Skip to content

Commit dccdc7b

Browse files
agrego lectura en voz alta y lectura en silencio
1 parent 10d1dd7 commit dccdc7b

3 files changed

Lines changed: 156 additions & 13 deletions

File tree

css/style.css

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,22 @@
207207

208208
/* Barra del tracker fija encima del scroll */
209209
#sticky-tracker-bar {
210+
display: flex;
211+
flex-wrap: wrap;
212+
justify-content: space-between;
213+
align-items: center;
210214
background: var(--primary);
211215
color: white;
212-
padding: 6px 14px;
216+
padding: 8px 14px;
213217
flex-shrink: 0;
214218
z-index: 60;
215219
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
216-
align-items: center;
217-
gap: 6px;
220+
gap: 8px;
221+
}
222+
223+
#sticky-tracker-bar .tracker-info {
224+
min-width: 120px;
225+
flex: 1 1 auto;
218226
}
219227

220228
#sticky-tracker-bar button#btn-reading-toggle {
@@ -258,6 +266,36 @@
258266
padding: 3px 5px;
259267
}
260268

269+
#floating-read-btn {
270+
display: none;
271+
position: fixed;
272+
bottom: 18px;
273+
right: 18px;
274+
z-index: 210;
275+
width: 54px;
276+
height: 54px;
277+
border-radius: 50%;
278+
border: none;
279+
background: var(--secondary);
280+
color: white;
281+
box-shadow: 0 10px 24px rgba(0,0,0,0.25);
282+
cursor: pointer;
283+
font-size: 1.35rem;
284+
font-weight: bold;
285+
display: flex;
286+
align-items: center;
287+
justify-content: center;
288+
transition: transform 0.2s ease, background 0.2s ease;
289+
}
290+
291+
#floating-read-btn:hover {
292+
transform: translateY(-2px);
293+
}
294+
295+
#floating-read-btn.active {
296+
background: #ff7043;
297+
}
298+
261299
.trk-side-arrow {
262300
background: rgba(255,255,255,0.15) !important;
263301
border: none !important;

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ <h1 id="app-title" style="cursor: pointer; user-select: none;">
6262

6363
<section class="content">
6464
<div id="sticky-tracker-bar" style="display:none;"></div>
65+
<button id="floating-read-btn" title="Leer automático" aria-label="Leer automático" onclick="window.toggleReadingAutoPlay()"></button>
6566
<div id="range-selector-bar" style="display:none; background:var(--primary); color:white; padding:8px 16px; z-index:58; box-shadow:0 2px 6px rgba(0,0,0,0.2); justify-content:center; align-items:center; gap:10px; flex-wrap:wrap;"></div>
6667
<div id="scrollable-content">
6768
<button class="btn-back" id="btn-back" style="display: none;" title="Regresar"></button>

js/ui.js

Lines changed: 114 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)