diff --git a/background.js b/background.js index b895db0..526aeec 100644 --- a/background.js +++ b/background.js @@ -4,7 +4,25 @@ const GEMINI_MODEL = 'gemini-flash-lite-latest'; const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`; -// (Offscreen document logic removed in favor of iframe approach) +// ─── Offscreen Document Management ─────────────────────────────────────────── + +async function setupOffscreenDocument(path) { + const offscreenUrl = chrome.runtime.getURL(path); + const existingContexts = await chrome.runtime.getContexts({ + contextTypes: ['OFFSCREEN_DOCUMENT'], + documentUrls: [offscreenUrl] + }); + + if (existingContexts.length > 0) { + return; + } + + await chrome.offscreen.createDocument({ + url: path, + reasons: ['USER_MEDIA'], + justification: 'Recording microphone input for speech recognition', + }); +} chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'GEMINI_REQUEST') { @@ -22,7 +40,32 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } if (message.type === 'OPEN_PERMISSION_PAGE') { - chrome.tabs.create({ url: chrome.runtime.getURL('claude-arabic-voice/permission.html') }); + chrome.tabs.create({ url: chrome.runtime.getURL('permission.html') }); + return true; + } + + // --- Offscreen Document Relays for Popup --- + if (message.type === 'START_RECORDING_FROM_POPUP') { + setupOffscreenDocument('claude-arabic-voice/offscreen.html') + .then(() => { + chrome.runtime.sendMessage({ + type: 'START_RECORDING', + payload: message.payload + }, (response) => { + sendResponse(response || { success: true }); + }); + }) + .catch(err => { + console.error('Failed to setup offscreen doc', err); + sendResponse({ success: false, error: err.message }); + }); + return true; + } + + if (message.type === 'STOP_RECORDING_FROM_POPUP') { + chrome.runtime.sendMessage({ type: 'STOP_RECORDING' }, (response) => { + sendResponse(response || { success: true }); + }); return true; } }); diff --git a/popup.js b/popup.js index ff4b8f4..ce828fd 100644 --- a/popup.js +++ b/popup.js @@ -264,76 +264,22 @@ document.getElementById('autofill-btn').addEventListener('click', async () => { // ─── Voice Dictation ─────────────────────────────────────────────────────────── +// ─── Voice Dictation ─────────────────────────────────────────────────────────── + function initDictation() { - const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const statusEl = document.getElementById('dictation-status'); const micBtn = document.getElementById('dictation-mic-btn'); const resultArea = document.getElementById('dictation-result'); const copyBtn = document.getElementById('copy-dictation-btn'); - if (!SpeechRecognition) { - statusEl.textContent = '❌ المتصفح لا يدعم التعرف على الصوت'; - micBtn.disabled = true; - micBtn.style.opacity = '0.5'; - return; - } - - let recognition = new SpeechRecognition(); - recognition.continuous = true; - recognition.interimResults = true; - recognition.lang = 'ar-SA'; - let isRecording = false; let finalTranscript = ''; - - recognition.onstart = () => { - isRecording = true; - micBtn.style.background = 'linear-gradient(135deg, #ff4d6d, #c9184a)'; - micBtn.innerHTML = '🔴'; - micBtn.style.animation = 'claude-voice-pulse 1.5s infinite'; // We can add this via inline or just rely on color - statusEl.textContent = 'جارٍ الاستماع... اضغط للإيقاف'; - finalTranscript = ''; - resultArea.value = ''; - copyBtn.style.display = 'none'; - }; - - recognition.onresult = (event) => { - let interimTranscript = ''; - let currentFinal = ''; - - for (let i = event.resultIndex; i < event.results.length; i++) { - const transcript = event.results[i][0].transcript; - if (event.results[i].isFinal) { - currentFinal += transcript + ' '; - } else { - interimTranscript += transcript; - } - } - - finalTranscript += currentFinal; - resultArea.value = finalTranscript + interimTranscript; - }; - - recognition.onerror = (event) => { - console.error('Speech recognition error', event.error); - if (event.error === 'not-allowed') { - statusEl.textContent = '❌ يرجى السماح للميكروفون من إعدادات المتصفح'; - } else if (event.error !== 'no-speech') { - statusEl.textContent = '❌ خطأ: ' + event.error; - } - stopRecording(false); - }; - - recognition.onend = () => { - if (isRecording) { - stopRecording(true); - } - }; + let interimTranscript = ''; function stopRecording(process = true) { if (!isRecording) return; isRecording = false; - recognition.stop(); + chrome.runtime.sendMessage({ type: 'STOP_RECORDING_FROM_POPUP' }); micBtn.style.background = 'linear-gradient(135deg, var(--accent), #9b5de5)'; micBtn.innerHTML = '🎤'; @@ -346,18 +292,51 @@ function initDictation() { } } + // Listen to messages from the offscreen document via the background + chrome.runtime.onMessage.addListener((message) => { + if (message.type === 'OFFSCREEN_RECORDING_RESULT') { + interimTranscript = message.payload.interimText || ''; + finalTranscript = message.payload.finalText || ''; + resultArea.value = (finalTranscript + interimTranscript).trim(); + } else if (message.type === 'OFFSCREEN_RECORDING_ERROR') { + console.error('Speech recognition error', message.payload.error); + if (message.payload.error === 'not-allowed') { + statusEl.textContent = '❌ يرجى السماح للميكروفون من إعدادات المتصفح'; + chrome.tabs.create({ url: chrome.runtime.getURL('permission.html') }); + } else if (message.payload.error !== 'no-speech') { + statusEl.textContent = '❌ خطأ: ' + message.payload.error; + } + stopRecording(false); + } else if (message.type === 'OFFSCREEN_RECORDING_END') { + if (isRecording) { + stopRecording(true); + } + } + }); + micBtn.addEventListener('click', async () => { if (isRecording) { stopRecording(true); } else { - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - stream.getTracks().forEach(t => t.stop()); - recognition.start(); - } catch (err) { - statusEl.textContent = '❌ يجب السماح باستخدام الميكروفون'; - chrome.tabs.create({ url: chrome.runtime.getURL('permission.html') }); - } + isRecording = true; + micBtn.style.background = 'linear-gradient(135deg, #ff4d6d, #c9184a)'; + micBtn.innerHTML = '🔴'; + statusEl.textContent = 'جارٍ الاستماع... اضغط للإيقاف'; + finalTranscript = ''; + interimTranscript = ''; + resultArea.value = ''; + copyBtn.style.display = 'none'; + + chrome.runtime.sendMessage({ + type: 'START_RECORDING_FROM_POPUP', + payload: { language: 'ar-SA' } + }, (response) => { + if (!response || !response.success) { + console.error('Failed to start recording', response); + statusEl.textContent = '❌ فشل بدء التسجيل'; + stopRecording(false); + } + }); } });