// Main application JavaScript with PWA support document.addEventListener('DOMContentLoaded', function() { // Register service worker if ('serviceWorker' in navigator) { registerServiceWorker(); } // Initialize app initApp(); // Check for PWA installation prompts initInstallPrompt(); }); // Service Worker Registration async function registerServiceWorker() { try { const registration = await navigator.serviceWorker.register('/service-worker.js'); console.log('Service Worker registered with scope:', registration.scope); // Setup periodic sync if available if ('periodicSync' in registration) { // Request permission for background sync const status = await navigator.permissions.query({ name: 'periodic-background-sync', }); if (status.state === 'granted') { try { // Register for background sync to check for updates await registration.periodicSync.register('translation-updates', { minInterval: 24 * 60 * 60 * 1000, // once per day }); console.log('Periodic background sync registered'); } catch (error) { console.error('Periodic background sync could not be registered:', error); } } } // Setup push notification if available if ('PushManager' in window) { setupPushNotifications(registration); } } catch (error) { console.error('Service Worker registration failed:', error); } } // Initialize the main application function initApp() { // DOM elements const recordBtn = document.getElementById('recordBtn'); const translateBtn = document.getElementById('translateBtn'); const sourceText = document.getElementById('sourceText'); const translatedText = document.getElementById('translatedText'); const sourceLanguage = document.getElementById('sourceLanguage'); const targetLanguage = document.getElementById('targetLanguage'); const playSource = document.getElementById('playSource'); const playTranslation = document.getElementById('playTranslation'); const clearSource = document.getElementById('clearSource'); const clearTranslation = document.getElementById('clearTranslation'); const statusIndicator = document.getElementById('statusIndicator'); const progressContainer = document.getElementById('progressContainer'); const progressBar = document.getElementById('progressBar'); const audioPlayer = document.getElementById('audioPlayer'); const ttsServerAlert = document.getElementById('ttsServerAlert'); const ttsServerMessage = document.getElementById('ttsServerMessage'); const ttsServerUrl = document.getElementById('ttsServerUrl'); const ttsApiKey = document.getElementById('ttsApiKey'); const updateTtsServer = document.getElementById('updateTtsServer'); // Set initial values let isRecording = false; let mediaRecorder = null; let audioChunks = []; let currentSourceText = ''; let currentTranslationText = ''; let currentTtsServerUrl = ''; // Check TTS server status on page load checkTtsServer(); // Check for saved translations in IndexedDB loadSavedTranslations(); // Update TTS server URL and API key updateTtsServer.addEventListener('click', function() { const newUrl = ttsServerUrl.value.trim(); const newApiKey = ttsApiKey.value.trim(); if (!newUrl && !newApiKey) { alert('Please provide at least one value to update'); return; } const updateData = {}; if (newUrl) updateData.server_url = newUrl; if (newApiKey) updateData.api_key = newApiKey; fetch('/update_tts_config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }) .then(response => response.json()) .then(data => { if (data.success) { statusIndicator.textContent = 'TTS configuration updated'; // Save URL to localStorage but not the API key for security if (newUrl) localStorage.setItem('ttsServerUrl', newUrl); // Check TTS server with new configuration checkTtsServer(); } else { alert('Failed to update TTS configuration: ' + data.error); } }) .catch(error => { console.error('Failed to update TTS config:', error); alert('Failed to update TTS configuration. See console for details.'); }); }); // Make sure target language is different from source if (targetLanguage.options[0].value === sourceLanguage.value) { targetLanguage.selectedIndex = 1; } // Event listeners for language selection sourceLanguage.addEventListener('change', function() { if (targetLanguage.value === sourceLanguage.value) { for (let i = 0; i < targetLanguage.options.length; i++) { if (targetLanguage.options[i].value !== sourceLanguage.value) { targetLanguage.selectedIndex = i; break; } } } }); targetLanguage.addEventListener('change', function() { if (targetLanguage.value === sourceLanguage.value) { for (let i = 0; i < sourceLanguage.options.length; i++) { if (sourceLanguage.options[i].value !== targetLanguage.value) { sourceLanguage.selectedIndex = i; break; } } } }); // Record button click event recordBtn.addEventListener('click', function() { if (isRecording) { stopRecording(); } else { startRecording(); } }); // Function to start recording function startRecording() { navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { mediaRecorder = new MediaRecorder(stream); audioChunks = []; mediaRecorder.addEventListener('dataavailable', event => { audioChunks.push(event.data); }); mediaRecorder.addEventListener('stop', () => { const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); transcribeAudio(audioBlob); }); mediaRecorder.start(); isRecording = true; recordBtn.classList.add('recording'); recordBtn.classList.replace('btn-primary', 'btn-danger'); recordBtn.innerHTML = ''; statusIndicator.textContent = 'Recording... Click to stop'; }) .catch(error => { console.error('Error accessing microphone:', error); alert('Error accessing microphone. Please make sure you have given permission for microphone access.'); }); } // Function to stop recording function stopRecording() { mediaRecorder.stop(); isRecording = false; recordBtn.classList.remove('recording'); recordBtn.classList.replace('btn-danger', 'btn-primary'); recordBtn.innerHTML = ''; statusIndicator.textContent = 'Processing audio...'; // Stop all audio tracks mediaRecorder.stream.getTracks().forEach(track => track.stop()); } // Function to transcribe audio function transcribeAudio(audioBlob) { const formData = new FormData(); formData.append('audio', audioBlob); formData.append('source_lang', sourceLanguage.value); showProgress(); fetch('/transcribe', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { hideProgress(); if (data.success) { currentSourceText = data.text; sourceText.innerHTML = `

${data.text}

`; playSource.disabled = false; translateBtn.disabled = false; statusIndicator.textContent = 'Transcription complete'; // Cache the transcription in IndexedDB saveToIndexedDB('transcriptions', { text: data.text, language: sourceLanguage.value, timestamp: new Date().toISOString() }); } else { sourceText.innerHTML = `

Error: ${data.error}

`; statusIndicator.textContent = 'Transcription failed'; } }) .catch(error => { hideProgress(); console.error('Transcription error:', error); sourceText.innerHTML = `

Failed to transcribe audio. Please try again.

`; statusIndicator.textContent = 'Transcription failed'; }); } // Translate button click event translateBtn.addEventListener('click', function() { if (!currentSourceText) { return; } statusIndicator.textContent = 'Translating...'; showProgress(); fetch('/translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: currentSourceText, source_lang: sourceLanguage.value, target_lang: targetLanguage.value }) }) .then(response => response.json()) .then(data => { hideProgress(); if (data.success) { currentTranslationText = data.translation; translatedText.innerHTML = `

${data.translation}

`; playTranslation.disabled = false; statusIndicator.textContent = 'Translation complete'; // Cache the translation in IndexedDB saveToIndexedDB('translations', { sourceText: currentSourceText, sourceLanguage: sourceLanguage.value, targetText: data.translation, targetLanguage: targetLanguage.value, timestamp: new Date().toISOString() }); } else { translatedText.innerHTML = `

Error: ${data.error}

`; statusIndicator.textContent = 'Translation failed'; } }) .catch(error => { hideProgress(); console.error('Translation error:', error); translatedText.innerHTML = `

Failed to translate. Please try again.

`; statusIndicator.textContent = 'Translation failed'; }); }); // Play source text playSource.addEventListener('click', function() { if (!currentSourceText) return; playAudio(currentSourceText, sourceLanguage.value); statusIndicator.textContent = 'Playing source audio...'; }); // Play translation playTranslation.addEventListener('click', function() { if (!currentTranslationText) return; playAudio(currentTranslationText, targetLanguage.value); statusIndicator.textContent = 'Playing translation audio...'; }); // Function to play audio via TTS function playAudio(text, language) { showProgress(); fetch('/speak', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text, language: language }) }) .then(response => response.json()) .then(data => { hideProgress(); if (data.success) { audioPlayer.src = data.audio_url; audioPlayer.onended = function() { statusIndicator.textContent = 'Ready'; }; audioPlayer.play(); } else { statusIndicator.textContent = 'TTS failed'; // Show TTS server alert with error message ttsServerAlert.classList.remove('d-none'); ttsServerAlert.classList.remove('alert-success'); ttsServerAlert.classList.add('alert-warning'); ttsServerMessage.textContent = data.error; alert('Failed to play audio: ' + data.error); // Check TTS server status again checkTtsServer(); } }) .catch(error => { hideProgress(); console.error('TTS error:', error); statusIndicator.textContent = 'TTS failed'; // Show TTS server alert ttsServerAlert.classList.remove('d-none'); ttsServerAlert.classList.remove('alert-success'); ttsServerAlert.classList.add('alert-warning'); ttsServerMessage.textContent = 'Failed to connect to TTS server'; }); } // Clear buttons clearSource.addEventListener('click', function() { sourceText.innerHTML = '

Your transcribed text will appear here...

'; currentSourceText = ''; playSource.disabled = true; translateBtn.disabled = true; }); clearTranslation.addEventListener('click', function() { translatedText.innerHTML = '

Translation will appear here...

'; currentTranslationText = ''; playTranslation.disabled = true; }); // Function to check TTS server status function checkTtsServer() { fetch('/check_tts_server') .then(response => response.json()) .then(data => { currentTtsServerUrl = data.url; ttsServerUrl.value = currentTtsServerUrl; // Load saved API key if available const savedApiKey = localStorage.getItem('ttsApiKeySet'); if (savedApiKey === 'true') { ttsApiKey.placeholder = '••••••• (API key saved)'; } if (data.status === 'error' || data.status === 'auth_error') { ttsServerAlert.classList.remove('d-none'); ttsServerAlert.classList.remove('alert-success'); ttsServerAlert.classList.add('alert-warning'); ttsServerMessage.textContent = data.message; if (data.status === 'auth_error') { ttsServerMessage.textContent = 'Authentication error with TTS server. Please check your API key.'; } } else { ttsServerAlert.classList.remove('d-none'); ttsServerAlert.classList.remove('alert-warning'); ttsServerAlert.classList.add('alert-success'); ttsServerMessage.textContent = 'TTS server is online and ready.'; setTimeout(() => { ttsServerAlert.classList.add('d-none'); }, 3000); } }) .catch(error => { console.error('Failed to check TTS server:', error); ttsServerAlert.classList.remove('d-none'); ttsServerAlert.classList.remove('alert-success'); ttsServerAlert.classList.add('alert-warning'); ttsServerMessage.textContent = 'Failed to check TTS server status.'; }); } // Progress indicator functions function showProgress() { progressContainer.classList.remove('d-none'); let progress = 0; const interval = setInterval(() => { progress += 5; if (progress > 90) { clearInterval(interval); } progressBar.style.width = `${progress}%`; }, 100); progressBar.dataset.interval = interval; } function hideProgress() { const interval = progressBar.dataset.interval; if (interval) { clearInterval(Number(interval)); } progressBar.style.width = '100%'; setTimeout(() => { progressContainer.classList.add('d-none'); progressBar.style.width = '0%'; }, 500); } } // IndexedDB functions for offline data storage function openIndexedDB() { return new Promise((resolve, reject) => { const request = indexedDB.open('VoiceTranslatorDB', 1); request.onupgradeneeded = (event) => { const db = event.target.result; // Create stores for transcriptions and translations if (!db.objectStoreNames.contains('transcriptions')) { db.createObjectStore('transcriptions', { keyPath: 'timestamp' }); } if (!db.objectStoreNames.contains('translations')) { db.createObjectStore('translations', { keyPath: 'timestamp' }); } }; request.onsuccess = (event) => { resolve(event.target.result); }; request.onerror = (event) => { reject('IndexedDB error: ' + event.target.errorCode); }; }); } function saveToIndexedDB(storeName, data) { openIndexedDB().then(db => { const transaction = db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); store.add(data); }).catch(error => { console.error('Error saving to IndexedDB:', error); }); } function loadSavedTranslations() { openIndexedDB().then(db => { const transaction = db.transaction(['translations'], 'readonly'); const store = transaction.objectStore('translations'); const request = store.getAll(); request.onsuccess = (event) => { const translations = event.target.result; if (translations && translations.length > 0) { // Could add a history section or recently used translations console.log('Loaded saved translations:', translations.length); } }; }).catch(error => { console.error('Error loading from IndexedDB:', error); }); } // PWA installation prompt function initInstallPrompt() { let deferredPrompt; const installButton = document.createElement('button'); installButton.style.display = 'none'; installButton.classList.add('btn', 'btn-success', 'fixed-bottom', 'm-3'); installButton.innerHTML = 'Install Voice Translator '; document.body.appendChild(installButton); window.addEventListener('beforeinstallprompt', (e) => { // Prevent Chrome 67 and earlier from automatically showing the prompt e.preventDefault(); // Stash the event so it can be triggered later deferredPrompt = e; // Update UI to notify the user they can add to home screen installButton.style.display = 'block'; installButton.addEventListener('click', (e) => { // Hide our user interface that shows our install button installButton.style.display = 'none'; // Show the prompt deferredPrompt.prompt(); // Wait for the user to respond to the prompt deferredPrompt.userChoice.then((choiceResult) => { if (choiceResult.outcome === 'accepted') { console.log('User accepted the install prompt'); } else { console.log('User dismissed the install prompt'); } deferredPrompt = null; }); }); }); } // Push notification setup function setupPushNotifications(swRegistration) { // First check if we already have permission if (Notification.permission === 'granted') { console.log('Notification permission already granted'); subscribeToPushManager(swRegistration); } else if (Notification.permission !== 'denied') { // Otherwise, ask for permission Notification.requestPermission().then(function(permission) { if (permission === 'granted') { console.log('Notification permission granted'); subscribeToPushManager(swRegistration); } }); } } async function subscribeToPushManager(swRegistration) { try { // Get the server's public key const response = await fetch('/api/push-public-key'); const data = await response.json(); // Convert the base64 string to Uint8Array function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } const convertedVapidKey = urlBase64ToUint8Array(data.publicKey); // Subscribe to push notifications const subscription = await swRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: convertedVapidKey }); // Send the subscription details to the server await fetch('/api/push-subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(subscription) }); console.log('User is subscribed to push notifications'); } catch (error) { console.error('Failed to subscribe to push notifications:', error); } }