Spaces:
Running
Running
| document.addEventListener("DOMContentLoaded", function () { | |
| const lastChapter = localStorage.getItem("lastAccessedChapter"); | |
| console.log("Last Save Chapter: "+ lastChapter) | |
| if (lastChapter) { | |
| const chapter = JSON.parse(lastChapter); | |
| loadChapter({ chapterTitle: chapter.chapterTitle }); // Auto-load last accessed chapter | |
| } | |
| const loginLink = document.getElementById('login-link'); // Login button container | |
| const userInfoDiv = document.getElementById('user-info'); // User info container (after login) | |
| const logoutButton = document.getElementById('logout-button'); // Logout button | |
| const userNameDisplay = document.getElementById('user-name'); // Where to display the username | |
| const userButton = document.getElementById('user-button'); // User initials button | |
| const token = localStorage.getItem('authToken'); // Get token from localStorage | |
| const menuButton = document.getElementById("menuButton"); | |
| const sidebar = document.querySelector(".sidebar"); | |
| const overlay = document.querySelector(".overlay"); | |
| const content = document.querySelector("#content"); // Main content area | |
| const nextBtn = document.getElementById('nextChapterButton'); | |
| if (nextBtn) nextBtn.addEventListener('click', loadNextChapter); | |
| const loadingContainer = document.getElementById('loading-container'); // Loading container | |
| const loadingBar = document.getElementById('loading-bar'); // Loading bar | |
| const loadingText = document.getElementById('loading-text'); // Loading text | |
| // Show loading bar when the page starts | |
| loadingContainer.style.display = 'flex'; | |
| // Fetch and initialize data from MongoDB | |
| fetch('https://thevera-botveradb.hf.space/get_chapter_titles') | |
| // fetch('https://thevera-botveradb.hf.space/get_chapters') | |
| .then(response => response.json()) | |
| .then(responseData => { | |
| if (responseData.success) { | |
| initializePage(responseData.data); | |
| } else { | |
| console.error('Error loading data:', responseData.message); | |
| } | |
| // Hide the loading bar after data is loaded | |
| loadingContainer.style.display = 'none'; | |
| }) | |
| .catch(error => { | |
| console.error('Error loading data:', error); | |
| // Hide the loading bar in case of error | |
| loadingContainer.style.display = 'none'; | |
| }); | |
| if (token) { | |
| // Check token validity with backend | |
| fetch('https://thevera-botveradb.hf.space/validate_token', { | |
| method: 'GET', | |
| headers: { | |
| 'Authorization': `Bearer ${token}` | |
| } | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.message === "Token is valid!") { | |
| // Token is valid, decode it to get user info | |
| const userObject = jwt_decode(token); // Decode the token to get user info | |
| const email = userObject.email; // Assuming the email is stored in the token | |
| const username = email.split('@')[0]; // Extract username (first part of email) | |
| // Update UI with user's info | |
| userNameDisplay.innerText = username; // Display first part of email as username | |
| userInfoDiv.style.display = 'block'; // Show user info section | |
| // Change user button to show user's initials | |
| const userInitials = username.charAt(0).toUpperCase(); // Get first letter of username | |
| userButton.innerHTML = `<div class="user-initial">${userInitials}</div>`; // Display initials | |
| // Show the logout button and hide the login link | |
| logoutButton.style.display = 'inline'; | |
| loginLink.style.display = 'none'; | |
| // **Update UI for Mobile** | |
| document.getElementById('user-info-mobile').style.display = 'block'; | |
| document.getElementById('user-initials').innerText = userInitials; | |
| document.getElementById('user-name-mobile').innerText = email.split('@')[0]; | |
| document.getElementById('logout-button-mobile').style.display = 'block'; | |
| document.getElementById('loginButton').style.display = 'none'; | |
| } else { | |
| // Token is invalid, clear invalid token | |
| localStorage.removeItem('authToken'); | |
| console.log('Invalid token, user not logged in.'); | |
| loginLink.style.display = 'inline'; // Show login link | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| // Handle the error (e.g., network issues) | |
| localStorage.removeItem('authToken'); // Clear token in case of error | |
| loginLink.style.display = 'inline'; // Show login link | |
| }); | |
| } else { | |
| // No token found, show login link | |
| console.log('No token found, user not logged in.'); | |
| loginLink.style.display = 'inline'; | |
| } | |
| // Logout functionality | |
| if (logoutButton) { | |
| logoutButton.onclick = function() { | |
| localStorage.removeItem('authToken'); // Clear token | |
| window.location.reload(); // Refresh page to update UI | |
| }; | |
| } | |
| // Optional: If you want to show the dropdown when clicking on the user initials button | |
| if (userButton) { | |
| userButton.addEventListener('click', () => { | |
| const userDropdown = document.getElementById('user-dropdown'); | |
| userDropdown.style.display = userDropdown.style.display === 'block' ? 'none' : 'block'; | |
| }); | |
| } | |
| // Sidebar toggle for mobile and desktop | |
| menuButton.addEventListener("click", function () { | |
| if (sidebar && overlay && content) { | |
| sidebar.classList.toggle("shown"); | |
| overlay.classList.toggle("active"); | |
| // Adjust content margin when sidebar is visible | |
| if (sidebar.classList.contains("shown")) { | |
| content.style.marginLeft = "250px"; // Sidebar width | |
| } else { | |
| content.style.marginLeft = "0"; | |
| } | |
| } | |
| }); | |
| // Close sidebar on overlay click for mobile view | |
| overlay.addEventListener("click", function () { | |
| if (sidebar && overlay && content) { | |
| sidebar.classList.remove("shown"); | |
| overlay.classList.remove("active"); | |
| content.style.marginLeft = "0"; // Reset content margin | |
| } | |
| }); | |
| }); | |
| // Mobile Menu Buttion js code | |
| document.addEventListener("DOMContentLoaded", function () { | |
| const menuButton = document.getElementById("menuButton"); | |
| const mobileMenu = document.getElementById("mobileMenu"); | |
| if (menuButton && mobileMenu) { | |
| menuButton.addEventListener("click", function (event) { | |
| event.stopPropagation(); // Prevents immediate close | |
| mobileMenu.classList.toggle("show"); // Toggle menu visibility | |
| }); | |
| // Close menu when clicking outside | |
| document.addEventListener("click", function (event) { | |
| if (!mobileMenu.contains(event.target) && !menuButton.contains(event.target)) { | |
| mobileMenu.classList.remove("show"); | |
| } | |
| }); | |
| } else { | |
| console.error("Menu button or mobile menu not found in the DOM."); | |
| } | |
| }); | |
| // ===== Voice Manager (persisted TTS settings) ===== | |
| // const TTS_STORE_KEY = 'ttsSettings_v1'; | |
| // const defaultTTS = { voiceURI: null, rate: 0.9, pitch: 0.9 }; | |
| // function getTTSSettings(){ | |
| // try { return { ...defaultTTS, ...(JSON.parse(localStorage.getItem(TTS_STORE_KEY))||{}) }; } | |
| // catch { return { ...defaultTTS }; } | |
| // } | |
| // function saveTTSSettings(patch){ | |
| // const now = { ...getTTSSettings(), ...(patch||{}) }; | |
| // localStorage.setItem(TTS_STORE_KEY, JSON.stringify(now)); | |
| // return now; | |
| // } | |
| // function waitForVoices(timeoutMs = 1500) { | |
| // return new Promise(resolve => { | |
| // const have = speechSynthesis.getVoices(); | |
| // if (have && have.length) return resolve(have); | |
| // const onVoices = () => { | |
| // const v = speechSynthesis.getVoices(); | |
| // if (v && v.length) { | |
| // speechSynthesis.removeEventListener('voiceschanged', onVoices); | |
| // resolve(v); | |
| // } | |
| // }; | |
| // speechSynthesis.addEventListener('voiceschanged', onVoices); | |
| // setTimeout(() => { | |
| // speechSynthesis.removeEventListener('voiceschanged', onVoices); | |
| // resolve(speechSynthesis.getVoices() || []); | |
| // }, timeoutMs); | |
| // }); | |
| // } | |
| // function preferredSort(a,b){ | |
| // // Prefer sa-IN > hi-IN > en-IN, then by name | |
| // const rank = v => (/sa-IN/i.test(v.lang)?0:(/hi-IN/i.test(v.lang)?1:(/en-IN/i.test(v.lang)?2:3))); | |
| // const r = rank(a)-rank(b); | |
| // if (r!==0) return r; return a.name.localeCompare(b.name); | |
| // } | |
| // async function populateVoiceSelect(){ | |
| // const select = document.getElementById('voiceSelect'); | |
| // if (!select) return; | |
| // select.innerHTML = ''; | |
| // const voices = await waitForVoices(); | |
| // voices.sort(preferredSort).forEach(v=>{ | |
| // const opt = document.createElement('option'); | |
| // opt.value = v.voiceURI; // persistable id | |
| // opt.textContent = `${v.name} — ${v.lang}`; | |
| // select.appendChild(opt); | |
| // }); | |
| // const st = getTTSSettings(); | |
| // if (st.voiceURI){ | |
| // const found = [...select.options].some(o=>o.value===st.voiceURI); | |
| // if (found) select.value = st.voiceURI; | |
| // } else if (voices.length){ | |
| // // auto-pick first preferred | |
| // saveTTSSettings({ voiceURI: voices[0].voiceURI }); | |
| // select.value = voices[0].voiceURI; | |
| // } | |
| // } | |
| // function attachVoiceUI(){ | |
| // const select = document.getElementById('voiceSelect'); | |
| // const refresh = document.getElementById('voiceRefresh'); | |
| // const sample = document.getElementById('voiceSample'); | |
| // const rate = document.getElementById('voiceRate'); | |
| // const pitch = document.getElementById('voicePitch'); | |
| // const st = getTTSSettings(); | |
| // if (rate) rate.value = st.rate; if (pitch) pitch.value = st.pitch; | |
| // if (select) select.addEventListener('change', ()=> saveTTSSettings({ voiceURI: select.value })); | |
| // if (rate) rate.addEventListener('input', ()=> saveTTSSettings({ rate: parseFloat(rate.value) })); | |
| // if (pitch) pitch.addEventListener('input', ()=> saveTTSSettings({ pitch: parseFloat(pitch.value) })); | |
| // if (refresh) refresh.addEventListener('click', populateVoiceSelect); | |
| // if (sample) sample.addEventListener('click', async ()=>{ | |
| // try { await speakClientSide("ॐ श्रीं नमः | यह एक नमूना आवाज़ है ।"); } catch(e){ console.warn(e); } | |
| // }); | |
| // } | |
| // function resolveSelectedVoice(voices){ | |
| // const st = getTTSSettings(); | |
| // let v = voices.find(v=>v.voiceURI===st.voiceURI); | |
| // if (!v){ | |
| // // best-effort locale pick | |
| // v = voices.sort(preferredSort)[0] || null; | |
| // if (v) saveTTSSettings({ voiceURI: v.voiceURI }); | |
| // } | |
| // return v; | |
| // } | |
| // Call once on load (safe no-op if controls are not present) | |
| // (function initVoiceControls(){ | |
| // if (!('speechSynthesis' in window)) return; | |
| // populateVoiceSelect(); | |
| // attachVoiceUI(); | |
| // })(); | |
| // Handle toggle for "Shastra Collection" header | |
| document.getElementById('shastraHeader').addEventListener('click', function() { | |
| const shastraList = document.getElementById('shastraList'); | |
| shastraList.classList.toggle('hidden'); // Toggle the 'hidden' class on the sidebar | |
| }); | |
| let chaptersArray = []; | |
| let currentChapterIndex = -1; // Initialize with -1 as default to indicate no chapter is loaded yet | |
| let resumeTime = 0; | |
| let player; | |
| function initializePage(data) { | |
| const shastraList = document.getElementById('shastraList'); | |
| if (!shastraList) { | |
| console.error("Element with ID 'shastraList' not found."); | |
| return; | |
| } | |
| Object.keys(data).forEach((collection) => { | |
| // Create main group (e.g., Bhagwat Mahapuran) | |
| const mainGroupItem = document.createElement('li'); | |
| mainGroupItem.classList.add('main-group'); | |
| mainGroupItem.innerHTML = ` | |
| <i class="fas fa-book" style="color:#4b6cb7;"></i> | |
| <span class="group-title">${collection}</span> | |
| `; | |
| const itemList = document.createElement('ul'); | |
| itemList.classList.add('nested-list'); | |
| itemList.style.display = 'none'; | |
| // Toggle display of main group chapters | |
| mainGroupItem.addEventListener('click', () => { | |
| itemList.style.display = itemList.style.display === 'none' ? 'block' : 'none'; | |
| }); | |
| data[collection].forEach((item) => { | |
| if (item.skandaTitle) { | |
| // Create Skanda group with + icon | |
| const skandaGroup = document.createElement('li'); | |
| skandaGroup.classList.add('skanda-group'); | |
| skandaGroup.innerHTML = ` | |
| <i class="fas fa-plus toggle-icon" style="color:#888;"></i> | |
| <i class="fas fa-folder" style="color:#f4b400;"></i> | |
| <span class="skanda-title">${item.skandaTitle}</span> | |
| `; | |
| const skandaChapterList = document.createElement('ul'); | |
| skandaChapterList.classList.add('nested-list'); | |
| skandaChapterList.style.display = 'none'; | |
| item.chapters.forEach((chapter) => { | |
| const chapterItem = document.createElement('li'); | |
| chapterItem.classList.add('chapter-item'); | |
| chapterItem.innerHTML = ` | |
| <i class="fas fa-file-alt" style="color:#5e5e5e;"></i> | |
| ${chapter.chapterTitle} | |
| `; | |
| chapterItem.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| fetchChapterData(chapter.chapterTitle).then(chapterDetails => { | |
| if (chapterDetails) { | |
| loadChapter(chapterDetails); | |
| closeSidebarOnMobile(); | |
| } | |
| }); | |
| }); | |
| skandaChapterList.appendChild(chapterItem); | |
| chaptersArray.push({ chapterTitle: chapter.chapterTitle }); | |
| }); | |
| skandaGroup.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| skandaChapterList.style.display = skandaChapterList.style.display === 'none' ? 'block' : 'none'; | |
| const toggleIcon = skandaGroup.querySelector('.toggle-icon'); | |
| if (toggleIcon) { | |
| toggleIcon.classList.toggle('fa-plus'); | |
| toggleIcon.classList.toggle('fa-minus'); | |
| } | |
| }); | |
| skandaGroup.appendChild(skandaChapterList); | |
| itemList.appendChild(skandaGroup); | |
| } else { | |
| const chapterItem = document.createElement('li'); | |
| chapterItem.classList.add('chapter-item'); | |
| chapterItem.innerHTML = `<i class="fas fa-file-alt"></i> ${item.chapterTitle}`; | |
| chapterItem.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| fetchChapterData(item.chapterTitle).then(chapterDetails => { | |
| if (chapterDetails) { | |
| loadChapter(chapterDetails); | |
| closeSidebarOnMobile(); | |
| } | |
| }); | |
| }); | |
| itemList.appendChild(chapterItem); | |
| chaptersArray.push({ chapterTitle: item.chapterTitle }); | |
| } | |
| }); | |
| mainGroupItem.appendChild(itemList); | |
| shastraList.appendChild(mainGroupItem); | |
| }); | |
| // Place this right after the forEach loop ends inside initializePage | |
| if (!localStorage.getItem("menuTipShown")) { | |
| const tip = document.createElement('div'); | |
| tip.innerText = "📚 Browse chapters here!"; | |
| tip.style.cssText = ` | |
| position: fixed; | |
| left: 20px; | |
| top: 70px; | |
| background: #fff8e1; | |
| padding: 8px 12px; | |
| border-radius: 5px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.2); | |
| z-index: 9999; | |
| font-weight: bold; | |
| `; | |
| document.body.appendChild(tip); | |
| setTimeout(() => { | |
| tip.remove(); | |
| localStorage.setItem("menuTipShown", "true"); | |
| }, 4000); | |
| } | |
| } | |
| function extractSkandaNumber(skandaTitle) { | |
| // e.g. "Skanda 1" or "Skanda One" or "Skanda I" | |
| const match = skandaTitle?.match(/(\d+)/); | |
| return match ? parseInt(match[1], 10) : 0; | |
| } | |
| function initializePageOld(data) { | |
| const shastraList = document.getElementById('shastraList'); | |
| if (!shastraList) { | |
| console.error("Element with ID 'shastraList' not found."); | |
| return; | |
| } | |
| Object.keys(data).forEach((collection) => { | |
| const mainGroupItem = document.createElement('li'); | |
| mainGroupItem.textContent = collection; | |
| mainGroupItem.classList.add('main-group'); | |
| const itemList = document.createElement('ul'); | |
| itemList.classList.add('nested-list'); | |
| itemList.style.display = 'none'; | |
| data[collection].forEach((item) => { | |
| if (item.skandaTitle) { | |
| const skandaGroup = document.createElement('li'); | |
| // skandaGroup.textContent = item.skandaTitle; | |
| skandaGroup.innerHTML = `<i class="fas fa-folder"></i> ${item.skandaTitle}`; | |
| skandaGroup.classList.add('skanda-group'); | |
| const skandaChapterList = document.createElement('ul'); | |
| skandaChapterList.classList.add('nested-list'); | |
| skandaChapterList.style.display = 'none'; | |
| item.chapters.forEach((chapter) => { | |
| const chapterItem = document.createElement('li'); | |
| // chapterItem.textContent = chapter.chapterTitle; | |
| chapterItem.innerHTML = `<i class="fas fa-file-alt"></i> ${chapter.chapterTitle}`; | |
| chapterItem.classList.add('chapter-item'); | |
| chapterItem.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| fetchChapterData(chapter.chapterTitle).then(chapterDetails => { | |
| if (chapterDetails) { | |
| loadChapter(chapterDetails); | |
| closeSidebarOnMobile(); | |
| } | |
| }); | |
| }); | |
| skandaChapterList.appendChild(chapterItem); | |
| // Populate chaptersArray with chapter metadata (title only for now) | |
| chaptersArray.push({ chapterTitle: chapter.chapterTitle }); | |
| }); | |
| skandaGroup.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| skandaChapterList.style.display = skandaChapterList.style.display === 'none' ? 'block' : 'none'; | |
| }); | |
| skandaGroup.appendChild(skandaChapterList); | |
| itemList.appendChild(skandaGroup); | |
| } else { | |
| const chapterItem = document.createElement('li'); | |
| chapterItem.textContent = item.chapterTitle; | |
| chapterItem.classList.add('chapter-item'); | |
| chapterItem.addEventListener('click', (event) => { | |
| event.stopPropagation(); | |
| fetchChapterData(item.chapterTitle).then(chapterDetails => { | |
| if (chapterDetails) { | |
| loadChapter(chapterDetails); | |
| closeSidebarOnMobile(); | |
| } | |
| }); | |
| }); | |
| itemList.appendChild(chapterItem); | |
| // Populate chaptersArray with chapter metadata (title only for now) | |
| chaptersArray.push({ chapterTitle: item.chapterTitle }); | |
| } | |
| }); | |
| mainGroupItem.addEventListener('click', () => { | |
| itemList.style.display = itemList.style.display === 'none' ? 'block' : 'none'; | |
| }); | |
| mainGroupItem.appendChild(itemList); | |
| shastraList.appendChild(mainGroupItem); | |
| }); | |
| } | |
| async function fetchChapterData(chapterTitle) { | |
| try { | |
| const response = await fetch(`https://thevera-botveradb.hf.space/get_chapter_details?chapterTitle=${encodeURIComponent(chapterTitle)}`); | |
| const result = await response.json(); | |
| if (response.ok && result.success) { | |
| return result.data; // Return chapter details | |
| } else { | |
| console.error("Error fetching chapter details:", result.message); | |
| return null; | |
| } | |
| } catch (error) { | |
| console.error("Error in fetchChapterData:", error); | |
| return null; | |
| } | |
| } | |
| // Function to fetch audio URL | |
| async function fetchAudioUrl(chapterTitle) { | |
| try { | |
| const response = await fetch(`https://thevera-botveradb.hf.space/get_audio_url?chapterTitle=${encodeURIComponent(chapterTitle)}`); | |
| const result = await response.json(); | |
| if (response.ok && result.success) { | |
| return result.audioUrl; | |
| } else { | |
| console.error('Error fetching audio:', result.message); | |
| return null; | |
| } | |
| } catch (error) { | |
| console.error('Error fetching audio:', error); | |
| return null; | |
| } | |
| } | |
| async function loadChapter(chapter) { | |
| console.log("Loading chapter..."); | |
| console.log(chapter.chapterTitle); | |
| // Save the last accessed chapter in localStorage | |
| localStorage.setItem("lastAccessedChapter", JSON.stringify({ | |
| chapterTitle: chapter.chapterTitle | |
| })); | |
| const chapterTitle = document.getElementById('chapterTitle'); | |
| const slokeContent = document.getElementById('slokeContent'); | |
| const videoPlayer = document.getElementById('videoPlayer'); | |
| const transcriptBlock = document.getElementById('transcript-content'); | |
| // Ensure the elements are loaded correctly | |
| if (!chapterTitle || !slokeContent || !videoPlayer || !transcriptBlock) { | |
| console.error("Missing elements in chapter display."); | |
| return; | |
| } | |
| // Reset content | |
| chapterTitle.textContent = 'Loading...'; // Temporary loading message | |
| slokeContent.innerHTML = ''; | |
| videoPlayer.src = ''; | |
| transcriptBlock.innerHTML = ''; // Clear all transcript content | |
| try { | |
| // Fetch chapter details from the backend | |
| const chapterDetails = await fetchChapterData(chapter.chapterTitle); | |
| if (!chapterDetails) { | |
| chapterTitle.textContent = 'Chapter details not found.'; | |
| return; | |
| } | |
| // Update UI with chapter details | |
| chapterTitle.textContent = chapterDetails.chapterTitle + " / " + chapterDetails.about || 'Chapter Details'; | |
| // Process and display the slokes | |
| const slokesArray = chapterDetails.slokes ? chapterDetails.slokes.split('\n') : []; | |
| slokesArray.forEach((slokeText) => { | |
| const slokeElement = document.createElement('p'); | |
| slokeElement.classList.add('sloke-text'); | |
| // slokeElement.innerHTML = slokeText.replace(/श्लोक \d+:/g, '<strong>$&</strong>'); | |
| slokeElement.innerHTML = slokeText.replace(/(श्लोक \d+:|अर्थ \d+:)/g, '<strong>$&</strong>'); | |
| const speakIcon = createSpeakIcon(slokeText); | |
| slokeElement.appendChild(speakIcon); | |
| slokeContent.appendChild(slokeElement); | |
| }); | |
| // Update video and transcript blocks | |
| // videoPlayer.src = chapterDetails.video || ''; | |
| loadYouTubeVideo(chapterDetails.video); | |
| const transcriptArray = chapterDetails.transcript ? chapterDetails.transcript.split('\n') : []; | |
| transcriptArray.forEach((transcriptText) => { | |
| const transcriptElement = document.createElement('p'); | |
| transcriptElement.classList.add('transcript-text'); | |
| transcriptElement.textContent = transcriptText; // Add transcript text | |
| transcriptBlock.appendChild(transcriptElement); | |
| }); | |
| // Show the content blocks | |
| document.querySelector('.main').classList.add('active'); | |
| document.querySelector('.sloke-block').classList.add('active'); | |
| document.querySelector('.video-block').classList.add('active'); | |
| document.querySelector('.transcript-block').classList.add('active'); | |
| // Setup play buttons | |
| setupPlayAllButton(slokesArray); | |
| setupPlayAllTranscriptButton(transcriptArray); | |
| // Auto-click to minimize the sidebar group | |
| const chapterGroup = document.querySelector(`.main-group`); | |
| if (chapterGroup) { | |
| chapterGroup.click(); // This simulates a click to collapse the group | |
| } | |
| // Update currentChapterIndex for "Next Chapter" functionality | |
| currentChapterIndex = chaptersArray.findIndex(ch => ch.chapterTitle === chapter.chapterTitle); | |
| console.log("Updated Current Index:", currentChapterIndex); | |
| document.getElementById('shastraHeader').click(); // Simulate a click on the header | |
| } catch (error) { | |
| console.error("Error loading chapter details:", error); | |
| chapterTitle.textContent = 'Failed to load chapter details. Please try again.'; | |
| } | |
| } | |
| function loadYouTubeVideo(videoUrl) { | |
| if (!videoUrl) { | |
| console.warn("[Video] No video URL provided"); | |
| return; | |
| } | |
| const videoIdMatch = videoUrl.match(/\/embed\/([a-zA-Z0-9_-]+)/); | |
| if (!videoIdMatch) { | |
| console.error("[Video] Invalid video URL format", videoUrl); | |
| return; | |
| } | |
| const videoId = videoIdMatch[1]; | |
| currentVideoId = videoId; | |
| const storageKey = `videoTime_${videoId}`; | |
| // resumeTime = parseFloat(localStorage.getItem(storageKey)) || 0; | |
| resumeTime = Math.max((parseFloat(localStorage.getItem(storageKey)) || 0) - 5, 0); | |
| console.log(`[Video] Loading ID: ${videoId}, Resuming at: ${resumeTime}s`); | |
| if (player) { | |
| player.loadVideoById({ videoId, startSeconds: resumeTime }); | |
| } else { | |
| player = new YT.Player('videoPlayer', { | |
| height: '315', | |
| width: '100%', | |
| videoId, | |
| playerVars: { | |
| autoplay: 1, | |
| controls: 1, | |
| start: resumeTime | |
| }, | |
| events: { | |
| 'onReady': (event) => { | |
| console.log("[Player] onReady"); | |
| event.target.seekTo(resumeTime, true); | |
| event.target.playVideo(); | |
| }, | |
| 'onStateChange': onPlayerStateChange | |
| } | |
| }); | |
| } | |
| } | |
| function onPlayerStateChange(event) { | |
| if (event.data === YT.PlayerState.PLAYING) { | |
| console.log("[Player] Playing"); | |
| // Save playback time every 5 seconds | |
| clearInterval(window.saveTimeInterval); | |
| window.saveTimeInterval = setInterval(() => { | |
| if (player && currentVideoId) { | |
| const time = player.getCurrentTime(); | |
| localStorage.setItem(`videoTime_${currentVideoId}`, time); | |
| console.log(`[Resume Save] ${currentVideoId}: ${time.toFixed(1)}s`); | |
| } | |
| }, 5000); | |
| } | |
| if (event.data === YT.PlayerState.ENDED) { | |
| console.log("[Player] Video Ended. Clearing resume time."); | |
| localStorage.removeItem(`videoTime_${currentVideoId}`); | |
| clearInterval(window.saveTimeInterval); | |
| } | |
| } | |
| function loadNextChapter() { | |
| console.log("Next Chapter clicked..."); | |
| if (currentChapterIndex + 1 < chaptersArray.length) { | |
| currentChapterIndex++; // Increment currentChapterIndex to move to the next chapter | |
| // Auto-click to minimize the sidebar group | |
| const chapterGroup = document.querySelector(`.main-group`); | |
| if (chapterGroup) { | |
| chapterGroup.click(); // This simulates a click to collapse the group | |
| } | |
| loadChapter(chaptersArray[currentChapterIndex]); | |
| } else { | |
| alert("This is the last chapter."); | |
| } | |
| } | |
| function closeSidebarOnMobile() { | |
| const sidebar = document.querySelector(".sidebar"); | |
| const overlay = document.querySelector(".overlay"); | |
| if (window.innerWidth < 768 && sidebar && overlay) { | |
| sidebar.classList.remove("shown"); | |
| overlay.classList.remove("active"); | |
| } | |
| } | |
| // Other functions such as createSpeakIcon, cleanSlokText, playTTS, and setupPlayAllButton remain as you provided | |
| function createSpeakIcon(slokeText) { | |
| const speakIcon = document.createElement('i'); | |
| speakIcon.classList.add('fas', 'fa-volume-up', 'speak-icon'); | |
| speakIcon.setAttribute('title', 'Speak Slok'); | |
| speakIcon.addEventListener('click', () => { | |
| console.log(`Speaking: ${slokeText}`); | |
| playTTS(slokeText); | |
| }); | |
| return speakIcon; | |
| } | |
| function cleanSlokText(text) { | |
| // Regex to match and remove introductory (e.g., "श्लोक 1:") and ending markers (e.g., "१") | |
| // return text.replace(/श्लोक \d+: /g, '').replace(/ \d+$/g, ''); | |
| return text.replace(/(श्लोक \d+:|अर्थ \d+:)/g,'').replace(/ \d+$/g, ''); | |
| } | |
| let currentSlokeIndex = 0; // Track the current sloke index | |
| const audioPlayer = document.getElementById('audioPlayer'); | |
| // Play all slokes function (replace your existing setupPlayAllButton function) | |
| function setupPlayAllButton(slokesArray) { | |
| const playAllButton = document.getElementById('playAllButton'); | |
| playAllButton.onclick = async () => { | |
| currentSlokeIndex = 0; // Reset to start | |
| playNextSloke(slokesArray); | |
| }; | |
| } | |
| // Function to play each sloke in sequence | |
| async function playNextSloke(slokesArray) { | |
| if (currentSlokeIndex >= slokesArray.length) { | |
| return; // End if all slokes have been played | |
| } | |
| // Clean and highlight the current sloke text | |
| // const slokeText = cleanSlokText(slokesArray[currentSlokeIndex]); | |
| const slokeText = slokesArray[currentSlokeIndex]; | |
| highlightCurrentSloke(currentSlokeIndex); | |
| // Generate TTS audio for the current sloke text | |
| playTTS(slokeText).then(() => { | |
| // Move to the next sloke after the current one finishes | |
| currentSlokeIndex++; | |
| playNextSloke(slokesArray); | |
| }); | |
| } | |
| // Function to highlight the current sloke and scroll it into view | |
| function highlightCurrentSloke(index) { | |
| const slokeElements = document.querySelectorAll('.sloke-text'); // Assume each sloke text has this class | |
| // Remove previous highlights | |
| slokeElements.forEach(el => el.classList.remove('highlight')); | |
| // Highlight and scroll the current sloke | |
| const currentSloke = slokeElements[index]; | |
| if (currentSloke) { | |
| currentSloke.classList.add('highlight'); | |
| currentSloke.scrollIntoView({ behavior: "smooth", block: "center" }); | |
| } | |
| } | |
| let currentTranscriptIndex = 0; // Track the current transcript index | |
| // Setup Play All for Transcript | |
| function setupPlayAllTranscriptButton(transcriptArray) { | |
| const playAllTranscriptButton = document.getElementById('playTranscriptButton'); | |
| playAllTranscriptButton.onclick = async () => { | |
| currentTranscriptIndex = 0; // Reset to start | |
| playNextTranscript(transcriptArray); | |
| }; | |
| } | |
| // Function to play each transcript in sequence | |
| async function playNextTranscript(transcriptArray) { | |
| if (currentTranscriptIndex >= transcriptArray.length) { | |
| return; // End if all transcripts have been played | |
| } | |
| // Highlight the current transcript text | |
| highlightCurrentTranscript(currentTranscriptIndex); | |
| // Generate TTS audio for the current transcript text | |
| const transcriptText = transcriptArray[currentTranscriptIndex]; | |
| playTTS(transcriptText).then(() => { | |
| // Move to the next transcript after the current one finishes | |
| currentTranscriptIndex++; | |
| playNextTranscript(transcriptArray); | |
| }); | |
| } | |
| // Function to highlight the current transcript text | |
| function highlightCurrentTranscript(index) { | |
| const allTranscripts = document.querySelectorAll('.transcript-text'); | |
| allTranscripts.forEach((transcript, i) => { | |
| if (i === index) { | |
| transcript.classList.add('highlight'); // Add highlight class | |
| } else { | |
| transcript.classList.remove('highlight'); // Remove highlight class | |
| } | |
| }); | |
| } | |
| let currentAudio; // Global variable to hold the audio object for server-generated audio | |
| let isPlaying = false; // Track play/pause state for playback | |
| let currentUtterance; // Track current SpeechSynthesisUtterance for sa-IN playback | |
| // --- Voice helpers --- | |
| function pickPreferredVoice(voices) { | |
| const tests = [ | |
| v => /sa-IN/i.test(v.lang) && /(male|deep|bass|india)/i.test(v.name), | |
| v => /sa-IN/i.test(v.lang), | |
| v => /hi-IN/i.test(v.lang) && /(male|deep|bass|india)/i.test(v.name), | |
| v => /hi-IN/i.test(v.lang), | |
| v => /en-IN/i.test(v.lang) && /(male|deep|bass|india)/i.test(v.name), | |
| v => /en-IN/i.test(v.lang), | |
| ]; | |
| for (const t of tests) { | |
| const found = voices.find(t); | |
| if (found) return found; | |
| } | |
| return voices[0] || null; | |
| } | |
| function waitForVoices(timeoutMs = 1500) { | |
| return new Promise(resolve => { | |
| const have = speechSynthesis.getVoices(); | |
| if (have && have.length) return resolve(have); | |
| const onVoices = () => { | |
| const v = speechSynthesis.getVoices(); | |
| if (v && v.length) { | |
| speechSynthesis.removeEventListener('voiceschanged', onVoices); | |
| resolve(v); | |
| } | |
| }; | |
| speechSynthesis.addEventListener('voiceschanged', onVoices); | |
| setTimeout(() => { | |
| speechSynthesis.removeEventListener('voiceschanged', onVoices); | |
| resolve(speechSynthesis.getVoices() || []); | |
| }, timeoutMs); | |
| }); | |
| } | |
| async function speakClientSide(text) { | |
| const voices = await waitForVoices(); | |
| if (!voices.length) throw new Error('No TTS voices available'); | |
| const voice = pickPreferredVoice(voices); | |
| const langFallback = voice?.lang || 'sa-IN'; | |
| // Stop any previous utterances | |
| try { speechSynthesis.cancel(); } catch {} | |
| // Split by pipe/comma and add small pauses after delimiters | |
| const parts = text.split(/(\||,)/).map(s => s.trim()).filter(Boolean); | |
| for (const chunk of parts) { | |
| const u = new SpeechSynthesisUtterance(chunk); | |
| if (voice) u.voice = voice; | |
| u.lang = voice?.lang || langFallback; | |
| u.rate = 0.9; // or your saved rate | |
| u.pitch = 0.9; // or your saved pitch | |
| u.volume = 1.0; | |
| await new Promise((res, rej) => { | |
| u.onend = res; | |
| u.onerror = rej; | |
| speechSynthesis.speak(u); | |
| }); | |
| if (chunk === '|' || chunk === ',') { | |
| await new Promise(r => setTimeout(r, 220)); | |
| } | |
| } | |
| } | |
| // Speak text in segments (pauses at "|" and ",") | |
| async function speakClientSideOld(text) { | |
| const st = ttsGet(); | |
| const all = await getAllVoices(); | |
| if (!all.length) throw new Error('No TTS voices available'); | |
| // pick language preference sa-IN -> hi-IN -> en-IN | |
| const prefLangs = ['sa-IN', 'hi-IN', 'en-IN']; | |
| let chosenLang = prefLangs.find(l => st.byLang[l]?.voiceURI) || prefLangs.find(l => all.some(v => v.lang && v.lang.startsWith(l))); | |
| if (!chosenLang) chosenLang = all[0].lang || 'hi-IN'; | |
| let voice = null; | |
| const savedURI = st.byLang[chosenLang]?.voiceURI; | |
| if (savedURI) voice = all.find(v => v.voiceURI === savedURI) || null; | |
| if (!voice) { | |
| const list = voicesForLang(all, chosenLang); | |
| voice = list[0] || all[0] || null; | |
| } | |
| const parts = text.split(/(\||,)/).map(s => s.trim()).filter(Boolean); | |
| for (const chunk of parts) { | |
| const u = new SpeechSynthesisUtterance(chunk); | |
| if (voice) u.voice = voice; | |
| u.lang = voice?.lang || chosenLang; | |
| u.rate = st.rate; | |
| u.pitch = st.pitch; | |
| await new Promise((res, rej)=>{ u.onend = res; u.onerror = rej; speechSynthesis.speak(u); }); | |
| if (chunk === '|' || chunk === ',') await new Promise(r=>setTimeout(r,220)); | |
| } | |
| } | |
| async function playTTS(text) { | |
| console.log('Speaking mantra'); | |
| if (!text || text.trim() === '') { | |
| console.error('No text provided'); | |
| return; | |
| } | |
| const cleanText = typeof cleanSlokText === 'function' ? cleanSlokText(text) : text; | |
| // Helper to play a URL in your existing <audio id="audioPlayer"> | |
| async function playAudioUrl(audioUrl, revokeWhenDone = false) { | |
| return new Promise((resolve, reject) => { | |
| const audioPlayer = document.getElementById('audioPlayer'); | |
| if (!audioPlayer) return reject(new Error('audioPlayer not found')); | |
| audioPlayer.onerror = (event) => reject(event); | |
| audioPlayer.onended = () => { | |
| try { if (revokeWhenDone && audioUrl.startsWith('blob:')) URL.revokeObjectURL(audioUrl); } catch {} | |
| resolve(); | |
| }; | |
| audioPlayer.src = audioUrl; | |
| audioPlayer.load(); | |
| audioPlayer.play().catch(reject); | |
| }); | |
| } | |
| // 1) Try saved audio (pass ORIGINAL text so backend can read “श्लोक N”) | |
| try { | |
| const savedUrl = await getAudioUrlForSloke(text); | |
| if (savedUrl) { | |
| console.log('Playing saved audio from:', savedUrl); | |
| await playAudioUrl(savedUrl); | |
| return; // done | |
| } | |
| } catch (e) { | |
| console.warn('Saved audio lookup failed:', e); | |
| // fall through to browser TTS | |
| } | |
| // 2) Browser TTS (client-side) | |
| try { | |
| if ('speechSynthesis' in window) { | |
| await speakClientSide(cleanText); // uses your voice picker or preference | |
| return; // done | |
| } | |
| } catch (e) { | |
| console.warn('Client-side TTS failed, will try server fallback:', e); | |
| } | |
| // 3) Server-side fallback | |
| try { | |
| const serverUrl = "https://thevera-botveradb.hf.space/generate_sanskrit_audio"; | |
| const response = await fetch(serverUrl, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ cleanText }) | |
| }); | |
| if (!response.ok) throw new Error('Server TTS fetch failed'); | |
| const audioBlob = await response.blob(); | |
| const blobUrl = URL.createObjectURL(audioBlob); | |
| await playAudioUrl(blobUrl, true); | |
| } catch (e) { | |
| console.error('Server TTS failed:', e); | |
| } | |
| } | |
| async function playTTSold(text) { | |
| console.log('Speaking mantra'); | |
| if (!text || text.trim() === '') { console.error('No text provided'); return; } | |
| const cleanText = typeof cleanSlokText === 'function' ? cleanSlokText(text) : text; | |
| // 1) Try saved audio first | |
| try { | |
| if (typeof getAudioUrlForSloke === 'function') { | |
| const savedUrl = await getAudioUrlForSloke(text); | |
| // const savedUrl = await getAudioUrlForSloke(cleanText); | |
| if (savedUrl) { | |
| console.log('Playing saved audio from:', savedUrl); | |
| await new Promise((resolve, reject) => { | |
| const audioPlayer = document.getElementById('audioPlayer'); | |
| audioPlayer.onerror = (event) => { console.error('Saved audio playback error:', event); reject(event); }; | |
| audioPlayer.onended = () => { isPlaying = false; if (savedUrl.startsWith('blob:')) URL.revokeObjectURL(savedUrl); resolve(); }; | |
| audioPlayer.src = savedUrl; audioPlayer.load(); isPlaying = true; audioPlayer.play(); | |
| }); | |
| return; | |
| } | |
| } | |
| } catch (e) { console.warn('Saved audio not found, trying client TTS...', e); } | |
| // 2) Browser TTS with selected voice | |
| try { | |
| if ('speechSynthesis' in window) { await speakClientSide(cleanText); return; } | |
| } catch (e) { console.warn('Client-side TTS failed, trying server TTS...', e); } | |
| // 3) Server fallback | |
| console.warn('Using server-side TTS fallback.'); | |
| const serverUrl = "https://thevera-botveradb.hf.space/generate_sanskrit_audio"; | |
| try { | |
| const response = await fetch(serverUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cleanText }) }); | |
| if (!response.ok) throw new Error('Failed to fetch audio from server'); | |
| const audioBlob = await response.blob(); const blobUrl = URL.createObjectURL(audioBlob); | |
| await new Promise((resolve, reject) => { | |
| const audioPlayer = document.getElementById('audioPlayer'); | |
| audioPlayer.onerror = (event) => { console.error('Server-based TTS playback error:', event); reject(event); }; | |
| audioPlayer.onended = () => { isPlaying = false; URL.revokeObjectURL(blobUrl); resolve(); }; | |
| audioPlayer.src = blobUrl; isPlaying = true; audioPlayer.play(); | |
| }); | |
| } catch (error) { console.error('Error calling server:', error); } | |
| } | |
| // Function to fetch the audio URL for a given sloke | |
| async function getAudioUrlForSlokeOld(text) { | |
| try { | |
| const response = await fetch(`https://thevera-botveradb.hf.space/get_audio_url?slokeText=${encodeURIComponent(text)}`); | |
| if (!response.ok) { | |
| throw new Error('Failed to fetch audio URL'); | |
| } | |
| const data = await response.json(); | |
| return data.audioUrl; // Assuming the API returns { audioUrl: 'URL' } | |
| } catch (error) { | |
| console.error('Error fetching audio URL:', error); | |
| return null; // Return null if an error occurs, so it falls back to server-side TTS | |
| } | |
| } | |
| async function getAudioUrlForSloke(originalText) { | |
| try { | |
| const url = `https://thevera-botveradb.hf.space/get_audio_url?slokeText=${encodeURIComponent(originalText)}`; | |
| const response = await fetch(url); | |
| // 404 just means "no saved audio" — return null quietly | |
| if (response.status === 404) return null; | |
| if (!response.ok) { | |
| // Other non-OK statuses: treat as no saved audio (no console noise) | |
| return null; | |
| } | |
| const data = await response.json(); | |
| // API returns { success, audioUrl } | |
| return data && data.success ? data.audioUrl : null; | |
| } catch { | |
| // Network or other errors → just say "no saved audio" | |
| return null; | |
| } | |
| } | |
| // Donate Model Function | |
| // Get elements | |
| const donateButton = document.getElementById('donateButton'); | |
| const donateModal = document.getElementById('donateModal'); | |
| const closeBtn = document.querySelector('.close'); | |
| const amountButtons = document.querySelectorAll('.amount-btn'); | |
| const customAmountInput = document.getElementById('customAmount'); | |
| const donateForm = document.getElementById('donateForm'); | |
| const openUpiAppButton = document.getElementById('openUpiApp'); | |
| // Donation amount logic | |
| let selectedAmount = 0; | |
| // Handle predefined amount buttons | |
| amountButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| selectedAmount = parseInt(button.dataset.amount); // Get amount from button's data attribute | |
| customAmountInput.value = ''; // Clear custom amount input | |
| amountButtons.forEach(btn => btn.classList.remove('selected')); // Deselect other buttons | |
| button.classList.add('selected'); // Highlight selected button | |
| }); | |
| }); | |
| // Handle custom amount input | |
| customAmountInput.addEventListener('input', () => { | |
| selectedAmount = parseInt(customAmountInput.value) || 0; // Set amount or default to 0 | |
| amountButtons.forEach(btn => btn.classList.remove('selected')); // Deselect predefined buttons | |
| }); | |
| // Open modal | |
| donateButton.onclick = function () { | |
| donateModal.style.display = "block"; // Show the modal | |
| } | |
| // Close modal | |
| closeBtn.onclick = function () { | |
| donateModal.style.display = "none"; // Hide the modal | |
| } | |
| // Close modal when clicking outside it | |
| window.onclick = function (event) { | |
| if (event.target === donateModal) { | |
| donateModal.style.display = "none"; | |
| } | |
| } | |
| // Submit form | |
| donateForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| // Get user details | |
| const name = document.getElementById('name').value.trim(); | |
| const email = document.getElementById('email').value.trim(); | |
| // Validate inputs | |
| if (!name || !email) { | |
| alert("Please enter your name and email."); | |
| return; | |
| } | |
| if (!selectedAmount || isNaN(selectedAmount) || selectedAmount <= 0) { | |
| alert("Please select or enter a valid donation amount."); | |
| return; | |
| } | |
| // Prepare donation data | |
| const donationData = { name, email, amount: selectedAmount }; | |
| try { | |
| logger.log(donationData) | |
| console.log(donationData) | |
| // Save to MongoDB via API | |
| const response = await fetch('https://thevera-botveradb.hf.space/save-donation', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(donationData), | |
| }); | |
| console.log(response) | |
| if (response.ok) { | |
| alert("Thank you for your donation!"); | |
| donateModal.style.display = "none"; | |
| } else { | |
| const errorData = await response.json(); | |
| alert(`Error: ${errorData.error || "Something went wrong. Please try again."}`); | |
| } | |
| } catch (error) { | |
| alert("Unable to process your donation. Please try again later."); | |
| console.error(error); | |
| } | |
| }); | |
| // Open UPI payment app | |
| openUpiAppButton.onclick = function () { | |
| if (!selectedAmount || isNaN(selectedAmount) || selectedAmount <= 0) { | |
| alert("Please select or enter a valid donation amount."); | |
| return; | |
| } | |
| // UPI payment link with amount | |
| const upiLink = `upi://pay?pa=sumandas24-4@okaxis&pn=Suman+Das&am=${selectedAmount}&cu=INR`; | |
| window.open(upiLink, '_blank'); | |
| }; | |
| // Share App Function | |
| function shareApp() { | |
| const shareData = { | |
| title: "BhagvatRas", | |
| text: "Explore BhagvatRas - Spiritual Insights and Discussions!", | |
| url: window.location.href | |
| }; | |
| // ✅ Web Share API (For Mobile & Modern Browsers) | |
| if (navigator.share) { | |
| navigator.share(shareData) | |
| .then(() => console.log("Shared successfully")) | |
| .catch((error) => console.log("Error sharing:", error)); | |
| } else { | |
| // ✅ Fallback: Manual share options | |
| let shareURL = encodeURIComponent(shareData.url); | |
| let shareText = encodeURIComponent(shareData.text); | |
| let logoURL = encodeURIComponent("https://bhagvat-ras.static.hf.space/icon_bhagvatras1.png"); | |
| let shareOptions = ` | |
| <p>Share this app:</p> | |
| <ul> | |
| <li><a href="mailto:?subject=${shareData.title}&body=${shareText}%20${shareURL}" target="_blank">📧 Email</a></li> | |
| <li><a href="https://api.whatsapp.com/send?text=${shareText}%20${shareURL}" target="_blank">📱 WhatsApp</a></li> | |
| <li><a href="https://www.facebook.com/sharer/sharer.php?u=${shareURL}" target="_blank">🔵 Facebook</a></li> | |
| <li><a href="https://twitter.com/intent/tweet?text=${shareText}&url=${shareURL}" target="_blank">🐦 Twitter</a></li> | |
| <li><a href="https://www.linkedin.com/sharing/share-offsite/?url=${shareURL}" target="_blank">💼 LinkedIn</a></li> | |
| </ul> | |
| <img src="${logoURL}" alt="BhagvatRas Logo" style="width: 50px; margin-top: 10px;"> | |
| `; | |
| // Show fallback share options | |
| let shareDiv = document.createElement("div"); | |
| shareDiv.innerHTML = shareOptions; | |
| shareDiv.style.position = "fixed"; | |
| shareDiv.style.top = "50px"; | |
| shareDiv.style.right = "20px"; | |
| shareDiv.style.background = "white"; | |
| shareDiv.style.padding = "10px"; | |
| shareDiv.style.border = "1px solid #ddd"; | |
| shareDiv.style.boxShadow = "0px 4px 6px rgba(0, 0, 0, 0.1)"; | |
| document.body.appendChild(shareDiv); | |
| } | |
| } | |
| // Install Function | |
| document.addEventListener("DOMContentLoaded", function () { | |
| let deferredPrompt; | |
| window.addEventListener('beforeinstallprompt', (e) => { | |
| e.preventDefault(); | |
| deferredPrompt = e; | |
| console.log("✅ beforeinstallprompt fired"); | |
| const installBtn = document.getElementById('installAppButton'); | |
| if (installBtn) { | |
| installBtn.style.display = 'block'; | |
| localStorage.setItem("installPromptShown", "true"); | |
| } | |
| }); | |
| const installBtn = document.getElementById('installAppButton'); | |
| if (installBtn) { | |
| installBtn.addEventListener('click', () => { | |
| if (deferredPrompt) { | |
| deferredPrompt.prompt(); | |
| deferredPrompt.userChoice.then((choiceResult) => { | |
| if (choiceResult.outcome === 'accepted') { | |
| console.log('✅ User accepted the A2HS prompt'); | |
| } else { | |
| console.log('❌ User dismissed the A2HS prompt'); | |
| } | |
| deferredPrompt = null; | |
| installBtn.style.display = 'none'; | |
| }); | |
| } | |
| }); | |
| } | |
| }); | |
| // ===== Voice Settings (popup) ===== | |
| // ===== Voice Settings (popup) — REPLACED & FIXED ===== | |
| const TTS_KEY = 'ttsSettings_v3'; | |
| // { byLang: { "hi-IN": {voiceURI, name}, ... }, rate, pitch, lastLang } | |
| const TTS_DEFAULT = { byLang: {}, rate: 0.9, pitch: 0.9, lastLang: null }; | |
| const ttsGet = () => { | |
| try { return { ...TTS_DEFAULT, ...(JSON.parse(localStorage.getItem(TTS_KEY)) || {}) }; } | |
| catch { return { ...TTS_DEFAULT }; } | |
| }; | |
| const ttsSave = (patch) => { | |
| const next = { ...ttsGet(), ...(patch || {}) }; | |
| localStorage.setItem(TTS_KEY, JSON.stringify(next)); | |
| return next; | |
| }; | |
| // Warm-up for mobile (iOS/Android) to force voices to load after a user gesture | |
| async function ensureVoicesReady() { | |
| let voices = speechSynthesis.getVoices(); | |
| if (voices && voices.length) return voices; | |
| // speak a tiny silent utterance to trigger load on some browsers | |
| try { | |
| const u = new SpeechSynthesisUtterance(' '); | |
| u.volume = 0; u.rate = 1; u.pitch = 1; | |
| speechSynthesis.speak(u); | |
| } catch {} | |
| // wait for voiceschanged or timeout | |
| voices = await new Promise(resolve => { | |
| const handler = () => { | |
| const all = speechSynthesis.getVoices(); | |
| if (all && all.length) { | |
| speechSynthesis.removeEventListener('voiceschanged', handler); | |
| resolve(all); | |
| } | |
| }; | |
| speechSynthesis.addEventListener('voiceschanged', handler); | |
| setTimeout(() => { | |
| speechSynthesis.removeEventListener('voiceschanged', handler); | |
| resolve(speechSynthesis.getVoices() || []); | |
| }, 2000); | |
| }); | |
| return voices; | |
| } | |
| function rankLang(a) { | |
| // Prefer IN locales, then Indic; otherwise alphabetical | |
| const priority = [ | |
| 'sa-IN','hi-IN','en-IN','bn-IN','mr-IN','gu-IN','ta-IN','te-IN','kn-IN','ml-IN','or-IN','pa-IN','as-IN' | |
| ]; | |
| const ia = priority.indexOf(a); | |
| if (ia >= 0) return ia; | |
| if (/-IN$/i.test(a)) return 50; // other India locales | |
| return 100; | |
| } | |
| function uniqueLangs(voices) { | |
| const set = new Set(); | |
| voices.forEach(v => v.lang && set.add(v.lang)); | |
| return [...set].sort((a,b) => { | |
| const r = rankLang(a) - rankLang(b); | |
| return r !== 0 ? r : a.localeCompare(b); | |
| }); | |
| } | |
| function voicesByLang(voices, lang) { | |
| return voices | |
| .filter(v => v.lang && v.lang.toLowerCase().startsWith(lang.toLowerCase())) | |
| .sort((a,b) => a.name.localeCompare(b.name)); | |
| } | |
| function openVoiceModal(){ document.getElementById('voiceModal')?.classList.add('show'); } | |
| function closeVoiceModal(){ document.getElementById('voiceModal')?.classList.remove('show'); } | |
| async function buildVoiceUI() { | |
| if (!('speechSynthesis' in window)) return; | |
| const langSel = document.getElementById('voiceLang'); | |
| const voiceSel = document.getElementById('voiceSelect'); | |
| const rate = document.getElementById('voiceRate'); | |
| const pitch = document.getElementById('voicePitch'); | |
| if (!langSel || !voiceSel) return; | |
| // ensure voices are actually present (mobile) | |
| let voices = await ensureVoicesReady(); | |
| if (!voices.length) { | |
| // one more retry on user action | |
| voices = await ensureVoicesReady(); | |
| } | |
| // populate languages dynamically | |
| const st = ttsGet(); | |
| const langs = uniqueLangs(voices); | |
| langSel.innerHTML = ''; | |
| langs.forEach(l => { | |
| const opt = document.createElement('option'); | |
| opt.value = l; opt.textContent = l; | |
| langSel.appendChild(opt); | |
| }); | |
| langSel.value = st.lastLang && langs.includes(st.lastLang) | |
| ? st.lastLang | |
| : (langs.find(l => /-IN$/i.test(l)) || langs[0] || ''); | |
| // populate voices for the chosen language | |
| function refreshVoiceList() { | |
| const lang = langSel.value; | |
| const list = voicesByLang(voices, lang); | |
| voiceSel.innerHTML = ''; | |
| list.forEach(v => { | |
| const opt = document.createElement('option'); | |
| opt.value = v.voiceURI; | |
| opt.textContent = `${v.name} — ${v.lang}`; | |
| voiceSel.appendChild(opt); | |
| }); | |
| // restore saved choice if available | |
| const savedUri = st.byLang[lang]?.voiceURI; | |
| if (savedUri && [...voiceSel.options].some(o => o.value === savedUri)) { | |
| voiceSel.value = savedUri; | |
| } else if (list[0]) { | |
| voiceSel.value = list[0].voiceURI; | |
| } | |
| } | |
| rate.value = st.rate; | |
| pitch.value = st.pitch; | |
| langSel.onchange = () => { ttsSave({ lastLang: langSel.value }); refreshVoiceList(); }; | |
| refreshVoiceList(); | |
| // sample play | |
| document.getElementById('voiceSample')?.addEventListener('click', async () => { | |
| try { | |
| const lang = langSel.value; | |
| const uri = voiceSel.value; | |
| const v = speechSynthesis.getVoices().find(x => x.voiceURI === uri) || null; | |
| speechSynthesis.cancel(); | |
| const u = new SpeechSynthesisUtterance( | |
| /en/i.test(lang) ? 'This is a sample voice.' : 'यह एक नमूना आवाज़ है | ॐ श्रीं नमः ।' | |
| ); | |
| if (v) u.voice = v; | |
| u.lang = v?.lang || lang; | |
| u.rate = parseFloat(rate.value); | |
| u.pitch = parseFloat(pitch.value); | |
| speechSynthesis.speak(u); | |
| } catch (e) { console.warn(e); } | |
| }); | |
| // save | |
| document.getElementById('voiceSave')?.addEventListener('click', () => { | |
| const lang = langSel.value; | |
| const uri = voiceSel.value; | |
| const v = speechSynthesis.getVoices().find(x => x.voiceURI === uri) || null; | |
| const byLang = { ...ttsGet().byLang, [lang]: { voiceURI: uri, name: v?.name || '' } }; | |
| ttsSave({ byLang, rate: parseFloat(rate.value), pitch: parseFloat(pitch.value), lastLang: lang }); | |
| closeVoiceModal(); | |
| }); | |
| // reset | |
| document.getElementById('voiceReset')?.addEventListener('click', () => { | |
| localStorage.removeItem(TTS_KEY); | |
| closeVoiceModal(); | |
| }); | |
| } | |
| // wire modal openers & close | |
| function wireVoiceButtons() { | |
| const btn1 = document.getElementById('voiceSettingsBtn'); | |
| const btn2 = document.getElementById('voiceSettingsBtnMobile'); | |
| const close = document.getElementById('voiceModalClose'); | |
| btn1?.addEventListener('click', () => { openVoiceModal(); buildVoiceUI(); }); | |
| btn2?.addEventListener('click', () => { openVoiceModal(); buildVoiceUI(); }); | |
| close?.addEventListener('click', closeVoiceModal); | |
| document.getElementById('voiceModal')?.addEventListener('click', e => { | |
| if (e.target && e.target.id === 'voiceModal') closeVoiceModal(); | |
| }); | |
| } | |
| document.addEventListener('DOMContentLoaded', wireVoiceButtons); | |
| // Make speakClientSide respect saved per-language voice, rate & pitch | |
| async function speakClientSide(text) { | |
| const st = ttsGet(); | |
| let voices = speechSynthesis.getVoices(); | |
| if (!voices || !voices.length) voices = await ensureVoicesReady(); | |
| if (!voices.length) throw new Error('No TTS voices available'); | |
| // preferred language: last used, else any -IN, else first | |
| const lang = st.lastLang || voices.find(v => /-IN$/i.test(v.lang))?.lang || voices[0].lang; | |
| let voice = null; | |
| const savedUri = st.byLang[lang]?.voiceURI; | |
| if (savedUri) voice = voices.find(v => v.voiceURI === savedUri) || null; | |
| if (!voice) voice = voicesByLang(voices, lang)[0] || voices[0]; | |
| // speak with small pauses at | and , | |
| speechSynthesis.cancel(); | |
| const parts = text.split(/(\||,)/).map(s => s.trim()).filter(Boolean); | |
| for (const chunk of parts) { | |
| const u = new SpeechSynthesisUtterance(chunk); | |
| if (voice) u.voice = voice; | |
| u.lang = voice?.lang || lang; | |
| u.rate = st.rate; | |
| u.pitch = st.pitch; | |
| await new Promise((res, rej) => { | |
| u.onend = res; u.onerror = rej; speechSynthesis.speak(u); | |
| }); | |
| if (chunk === '|' || chunk === ',') await new Promise(r => setTimeout(r, 220)); | |
| } | |
| } | |
| // ===== end Voice Settings (popup) ===== | |
| // Code for Satsang | |
| function checkLoginAndOpenSatsang() { | |
| const token = localStorage.getItem("authToken"); | |
| if (!token) { | |
| alert("Please login to access the Daily Satsang."); | |
| window.location.href = "login.html"; | |
| } else { | |
| window.location.href = "satsang.html"; | |
| } | |
| } | |