From 70450f25929f9fee571ce25af46ffcc513b06a66 Mon Sep 17 00:00:00 2001 From: Dnyaneshwar Bhajantri Date: Sat, 29 Nov 2025 05:17:49 +0530 Subject: [PATCH 1/3] commit the changes --- background.js | 3 ++- js/content.js | 52 +++++++++++++++++++++++++------------------------- js/download.js | 8 ++++++++ js/events.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++ js/popup.js | 24 ++++++++++++++++++++--- popup.html | 1 + 6 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 js/download.js diff --git a/background.js b/background.js index f5ca4f4..e1b982d 100644 --- a/background.js +++ b/background.js @@ -4,7 +4,8 @@ try { "js/defaults.js", "js/messaging.js", "js/content-handlers.js", - "js/events.js" + "js/events.js", + "js/download.js" ) } catch (err) { diff --git a/js/content.js b/js/content.js index c474317..8b12211 100644 --- a/js/content.js +++ b/js/content.js @@ -1,10 +1,10 @@ -(function() { +(function () { registerMessageListener("contentScript", { getRequireJs: getRequireJs, getDocumentInfo: getInfo, getCurrentIndex: getCurrentIndex, - getTexts: getTexts + getTexts: getTexts, }) function getInfo() { @@ -17,7 +17,7 @@ function getLang() { var lang = document.documentElement.lang || $("html").attr("xml:lang"); - if (lang) lang = lang.split(",",1)[0].replace(/_/g, '-'); + if (lang) lang = lang.split(",", 1)[0].replace(/_/g, '-'); return lang; } @@ -47,9 +47,9 @@ || $("embed[type='application/pdf']").length || $("iframe[src*='.pdf']").length) return ["js/content/pdf-doc.js"]; else if (/^\d+\.\d+\.\d+\.\d+$/.test(location.hostname) - && location.port === "1122" - && location.protocol === "http:" - && location.pathname === "/bookshelf/index.html") return ["js/content/yd-app-web.js"]; + && location.port === "1122" + && location.protocol === "http:" + && location.pathname === "/bookshelf/index.html") return ["js/content/yd-app-web.js"]; else return ["js/content/html-doc.js"]; } @@ -65,7 +65,7 @@ } else { return Promise.resolve(readAloudDoc.getTexts(index, quietly)) - .then(function(texts) { + .then(function (texts) { if (texts && Array.isArray(texts)) { if (!quietly) console.log(texts.join("\n\n")); } @@ -90,7 +90,7 @@ if (!audioCanPlay()) return; const silenceTrack = getSilenceTrack() try { - const should = await sendToPlayer({method: "shouldPlaySilence", args: [providerId]}) + const should = await sendToPlayer({ method: "shouldPlaySilence", args: [providerId] }) if (should) silenceTrack.start() else silenceTrack.stop() } @@ -128,7 +128,7 @@ function isNotEmpty(text) { function fixParagraphs(texts) { var out = []; var para = ""; - for (var i=0; i 0) return tryGetTexts(getTexts, millis-500); + .then(function (texts) { + if (texts && !texts.length && millis - 500 > 0) return tryGetTexts(getTexts, millis - 500); else return texts; }) } @@ -181,20 +181,20 @@ function simulateMouseEvent(element, eventName, coordX, coordY) { function simulateClick(elementToClick) { var box = elementToClick.getBoundingClientRect(), - coordX = box.left + (box.right - box.left) / 2, - coordY = box.top + (box.bottom - box.top) / 2; - simulateMouseEvent (elementToClick, "mousedown", coordX, coordY); - simulateMouseEvent (elementToClick, "mouseup", coordX, coordY); - simulateMouseEvent (elementToClick, "click", coordX, coordY); + coordX = box.left + (box.right - box.left) / 2, + coordY = box.top + (box.bottom - box.top) / 2; + simulateMouseEvent(elementToClick, "mousedown", coordX, coordY); + simulateMouseEvent(elementToClick, "mouseup", coordX, coordY); + simulateMouseEvent(elementToClick, "click", coordX, coordY); } -const getMath = (function() { +const getMath = (function () { let promise = Promise.resolve(null) return () => promise = promise.then(math => math || makeMath()) })(); async function makeMath() { - const getXmlFromMathEl = function(mathEl) { + const getXmlFromMathEl = function (mathEl) { const clone = mathEl.cloneNode(true) $("annotation, annotation-xml", clone).remove() removeAllAttrs(clone, true) @@ -210,11 +210,11 @@ async function makeMath() { return mathEl ? getXmlFromMathEl(mathEl) : el.getAttribute("data-mathml") }, }) - .when(() => document.querySelector("math"), { - selector: "math", - getXML: getXmlFromMathEl, - }) - .else(null) + .when(() => document.querySelector("math"), { + selector: "math", + getXML: getXmlFromMathEl, + }) + .else(null) if (!math) return null const elems = $(math.selector).get() @@ -229,8 +229,8 @@ async function makeMath() { catch (err) { console.error(err) return { - show() {}, - hide() {} + show() { }, + hide() { } } } diff --git a/js/download.js b/js/download.js new file mode 100644 index 0000000..0cd4c23 --- /dev/null +++ b/js/download.js @@ -0,0 +1,8 @@ +brapi.runtime.onMessage.addListener((msg, sender) => { + if (msg.action === "downloadSelectedText") { + console.log("Received selected text:", msg.text); + + // next step: convert to audio + startDownloadProcess(msg.text); + } +}); diff --git a/js/events.js b/js/events.js index d8849f9..879dd7f 100644 --- a/js/events.js +++ b/js/events.js @@ -22,11 +22,60 @@ var handlers = { reportIssue: reportIssue, authWavenet: authWavenet, managePiperVoices, + getSelectedTextFromTab: getSelectedTextFromTab } registerMessageListener("serviceWorker", handlers) +async function generateAudioBlob(text) { + // same voice/settings used for normal play + const settings = await getSettings(); + const rate = await getSetting("rate" + (settings.voiceName || "")); + + const speech = new Speech([text], { + rate: rate || defaults.rate, + pitch: settings.pitch || defaults.pitch, + volume: settings.volume || defaults.volume, + lang: settings.lang || "en-US" + }); + + // Convert speech to blob + return await speech.createAudioBlob(); +} + + +function blobToBase64(blob) { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(blob); + }); +} + + +async function getSelectedTextFromTab() { + const tab = await getActiveTab(); + if (!tab) return ""; + + // Try all frames + const results = await brapi.scripting.executeScript({ + target: { tabId: tab.id, allFrames: true }, + func: () => window.getSelection().toString().trim() + }); + + // Loop through results and return the first non-empty selection + for (const r of results) { + if (r && r.result && r.result.trim()) { + return r.result.trim(); + } + } + + // Nothing selected + return ""; +} + + /** * Installers */ diff --git a/js/popup.js b/js/popup.js index d0c91cf..e8442d2 100644 --- a/js/popup.js +++ b/js/popup.js @@ -68,7 +68,7 @@ async function init() { $("#decrease-window-size").click(changeWindowSize.bind(null, -1)); $("#increase-window-size").click(changeWindowSize.bind(null, +1)); $("#toggle-dark-mode").click(toggleDarkMode); - + $("#downloadBtn").click(onDownload); refreshSize(); checkAnnouncements(); @@ -76,8 +76,6 @@ async function init() { if (state == "PAUSED" || state == "STOPPED") onPlay() } - - function handleError(err) { if (!err) return; if (err.name == "CancellationException") return; @@ -320,6 +318,26 @@ function changeFontSize(delta) { .catch(handleError) } +async function onDownload() { + try { + const text = await bgPageInvoke("getSelectedTextFromTab"); + + if (!text) return alert("Please select some text first."); + + const base64Audio = await bgPageInvoke("downloadSelectedText", text); + + const a = document.createElement("a"); + a.href = base64Audio; + a.download = "readaloud.mp3"; + a.click(); + + } catch (err) { + console.error(err); + alert("Failed to download audio."); + } +} + + function changeWindowSize(delta) { getSettings(["highlightWindowSize"]) .then(function(settings) { diff --git a/popup.html b/popup.html index 5adbf61..c9fb8f7 100644 --- a/popup.html +++ b/popup.html @@ -26,6 +26,7 @@ stop settings + Download
From d98ce930868ecfa553e0efa6cf83fcb66963218e Mon Sep 17 00:00:00 2001 From: Dnyaneshwar Bhajantri Date: Tue, 2 Dec 2025 12:13:42 +0530 Subject: [PATCH 2/3] Changes made to add download feature --- background.js | 36 ++++++- js/download.js | 15 +-- js/events.js | 211 ++++++++++++++++++++------------------ js/player.js | 271 +++++++++++++++++++++++++++++++++++++++++-------- js/popup.js | 20 +++- js/speech.js | 1 + 6 files changed, 399 insertions(+), 155 deletions(-) diff --git a/background.js b/background.js index e1b982d..2ff9363 100644 --- a/background.js +++ b/background.js @@ -7,7 +7,39 @@ try { "js/events.js", "js/download.js" ) -} -catch (err) { +} catch (err) { console.error(err) } + +registerMessageListener("background", { + async downloadSelectedText(text) { + console.log("BG: synthesizing (no player)", text); + + const settings = await getSettings(["voiceName", "rate", "pitch", "voices"]); + const voice = (settings.voices || [])[0]; // fallback + + const options = { + voice, + lang: voice.lang, + rate: settings.rate, + pitch: settings.pitch + }; + + // Create speech instance WITHOUT player + const speech = new Speech([text], options); + + // Use engine directly + const engine = + speech.engine || + speech._engine || + speech.__engine; + + const url = await engine.getAudioUrl(text, options.voice, options.pitch); + + const blob = await fetch(url).then(r => r.blob()); + + return await blobToBase64(blob); + } +}); + + diff --git a/js/download.js b/js/download.js index 0cd4c23..d9bd8a4 100644 --- a/js/download.js +++ b/js/download.js @@ -1,8 +1,9 @@ -brapi.runtime.onMessage.addListener((msg, sender) => { - if (msg.action === "downloadSelectedText") { - console.log("Received selected text:", msg.text); +// brapi.runtime.onMessage.addListener((msg, sender) => { +// console.log("In downloads the message is received", msg); +// if (msg.action === "downloadSelectedText") { +// console.log("Received selected text:", msg.text); - // next step: convert to audio - startDownloadProcess(msg.text); - } -}); +// // next step: convert to audio +// startDownloadProcess(msg.text); +// } +// }); diff --git a/js/events.js b/js/events.js index 879dd7f..98fe2a0 100644 --- a/js/events.js +++ b/js/events.js @@ -1,5 +1,5 @@ -brapi.runtime.onInstalled.addListener(function() { +brapi.runtime.onInstalled.addListener(function () { installContentScripts() installContextMenus() }) @@ -22,60 +22,75 @@ var handlers = { reportIssue: reportIssue, authWavenet: authWavenet, managePiperVoices, - getSelectedTextFromTab: getSelectedTextFromTab + getSelectedText: getSelectedText, + downloadSelectedText: downloadSelectedText + } registerMessageListener("serviceWorker", handlers) async function generateAudioBlob(text) { - // same voice/settings used for normal play + // Get TTS settings const settings = await getSettings(); const rate = await getSetting("rate" + (settings.voiceName || "")); - + + // Choose language + let lang = settings.lang || "en-US"; + + // Create a Speech instance (same used for Read Aloud player) const speech = new Speech([text], { rate: rate || defaults.rate, pitch: settings.pitch || defaults.pitch, volume: settings.volume || defaults.volume, - lang: settings.lang || "en-US" + lang: lang }); - // Convert speech to blob + // Generate & return the audio blob return await speech.createAudioBlob(); } function blobToBase64(blob) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; reader.readAsDataURL(blob); }); } +async function getSelectedText() { + try { + const tabs = await brapi.tabs.query({ active: true, currentWindow: true }); + if (!tabs.length) return ""; -async function getSelectedTextFromTab() { - const tab = await getActiveTab(); - if (!tab) return ""; + const tabId = tabs[0].id; - // Try all frames - const results = await brapi.scripting.executeScript({ - target: { tabId: tab.id, allFrames: true }, - func: () => window.getSelection().toString().trim() - }); + const result = await brapi.scripting.executeScript({ + target: { tabId }, + func: () => window.getSelection().toString().trim(), + }); - // Loop through results and return the first non-empty selection - for (const r of results) { - if (r && r.result && r.result.trim()) { - return r.result.trim(); - } + return result[0].result || ""; + } catch (e) { + console.error("getSelectedText ERROR:", e); + return ""; } +}; + - // Nothing selected - return ""; +async function downloadSelectedText(text) { + console.log("BG → sending to player:", text); + return sendToPlayer({ + dest: "player", + method: "synthesizeDownloadAudio", + args: [text] + }); } + /** * Installers */ @@ -89,7 +104,7 @@ async function installContentScripts() { world: "MAIN" }, ] - const registeredIds = await brapi.scripting.getRegisteredContentScripts({ids: scripts.map(x => x.id)}) + const registeredIds = await brapi.scripting.getRegisteredContentScripts({ ids: scripts.map(x => x.id) }) .then(scripts => scripts.map(x => x.id)) .catch(err => { console.error(err) @@ -108,15 +123,15 @@ async function installContentScripts() { function installContextMenus() { if (brapi.contextMenus) - brapi.contextMenus.create({ - id: "read-selection", - title: brapi.i18n.getMessage("context_read_selection"), - contexts: ["selection"] - }, - function() { - if (brapi.runtime.lastError) console.error(brapi.runtime.lastError) - else console.info("Installed context menus") - }) + brapi.contextMenus.create({ + id: "read-selection", + title: brapi.i18n.getMessage("context_read_selection"), + contexts: ["selection"] + }, + function () { + if (brapi.runtime.lastError) console.error(brapi.runtime.lastError) + else console.info("Installed context menus") + }) } @@ -124,49 +139,49 @@ function installContextMenus() { * Context menu handlers */ if (brapi.contextMenus) -brapi.contextMenus.onClicked.addListener(function(info, tab) { - if (info.menuItemId == "read-selection") - Promise.resolve() - .then(function() { - if (tab && tab.id != -1) return detectTabLanguage(tab.id) - else return undefined - }) - .then(function(lang) { - return playText(info.selectionText, {lang: lang}) - }) - .catch(handleHeadlessError) -}) + brapi.contextMenus.onClicked.addListener(function (info, tab) { + if (info.menuItemId == "read-selection") + Promise.resolve() + .then(function () { + if (tab && tab.id != -1) return detectTabLanguage(tab.id) + else return undefined + }) + .then(function (lang) { + return playText(info.selectionText, { lang: lang }) + }) + .catch(handleHeadlessError) + }) /** * Shortcut keys handlers */ if (brapi.commands) -brapi.commands.onCommand.addListener(function(command) { - if (command == "play") { - getPlaybackState() - .then(function(stateInfo) { - switch (stateInfo.state) { - case "PLAYING": return pause() - case "PAUSED": return resume() - case "STOPPED": return playTab() - } - }) - .catch(handleHeadlessError) - } - else if (command == "stop") { - stop() - .catch(handleHeadlessError) - } - else if (command == "forward") { - forward() - .catch(handleHeadlessError) - } - else if (command == "rewind") { - rewind() - .catch(handleHeadlessError) - } -}) + brapi.commands.onCommand.addListener(function (command) { + if (command == "play") { + getPlaybackState() + .then(function (stateInfo) { + switch (stateInfo.state) { + case "PLAYING": return pause() + case "PAUSED": return resume() + case "STOPPED": return playTab() + } + }) + .catch(handleHeadlessError) + } + else if (command == "stop") { + stop() + .catch(handleHeadlessError) + } + else if (command == "forward") { + forward() + .catch(handleHeadlessError) + } + else if (command == "rewind") { + rewind() + .catch(handleHeadlessError) + } + }) /** @@ -229,24 +244,24 @@ var currentTask = { async function playText(text, opts) { const hasPlayer = await stop().then(res => res == true, err => false) if (!hasPlayer) await injectPlayer(await getActiveTab()) - await sendToPlayer({method: "playText", args: [text, opts]}) + await sendToPlayer({ method: "playText", args: [text, opts] }) } async function playTab(tabId) { const tab = tabId ? await getTab(tabId) : await getActiveTab() - if (!tab) throw new Error(JSON.stringify({code: "error_page_unreadable"})) + if (!tab) throw new Error(JSON.stringify({ code: "error_page_unreadable" })) const task = currentTask.begin() try { const handler = contentHandlers.find(h => h.match(tab.url || "", tab.title)) if (handler.validate) await handler.validate(tab) if (handler.getSourceUri) { - await brapi.storage.local.set({"sourceUri": handler.getSourceUri(tab)}) + await brapi.storage.local.set({ "sourceUri": handler.getSourceUri(tab) }) } else { const frameId = handler.getFrameId && await getAllFrames(tab.id).then(frames => handler.getFrameId(frames)) if (!await contentScriptAlreadyInjected(tab, frameId)) await injectContentScript(tab, frameId, handler.extraScripts) - await brapi.storage.local.set({"sourceUri": "contentscript:" + tab.id}) + await brapi.storage.local.set({ "sourceUri": "contentscript:" + tab.id }) } } finally { @@ -255,7 +270,7 @@ async function playTab(tabId) { const hasPlayer = await stop().then(res => res == true, err => false) if (!hasPlayer) await injectPlayer(tab) - await sendToPlayer({method: "playTab"}) + await sendToPlayer({ method: "playTab" }) } async function reloadAndPlayTab(tabId) { @@ -284,37 +299,37 @@ async function reloadAndPlayTab(tabId) { function stop() { currentTask.cancel() - return sendToPlayer({method: "stop"}) + return sendToPlayer({ method: "stop" }) } function pause() { - return sendToPlayer({method: "pause"}) + return sendToPlayer({ method: "pause" }) } function resume() { - return sendToPlayer({method: "resume"}) + return sendToPlayer({ method: "resume" }) } async function getPlaybackState() { - if (currentTask.isActive()) return {state: "LOADING"} + if (currentTask.isActive()) return { state: "LOADING" } try { - return await sendToPlayer({method: "getPlaybackState"}) || {state: "STOPPED"} + return await sendToPlayer({ method: "getPlaybackState" }) || { state: "STOPPED" } } catch (err) { - return {state: "STOPPED"} + return { state: "STOPPED" } } } function forward() { - return sendToPlayer({method: "forward"}) + return sendToPlayer({ method: "forward" }) } function rewind() { - return sendToPlayer({method: "rewind"}) + return sendToPlayer({ method: "rewind" }) } function seek(n) { - return sendToPlayer({method: "seek", args: [n]}) + return sendToPlayer({ method: "seek", args: [n] }) } @@ -327,7 +342,7 @@ function handleHeadlessError(err) { function reportIssue(url, comment) { var manifest = brapi.runtime.getManifest(); return getSettings() - .then(function(settings) { + .then(function (settings) { if (url) settings.url = url; settings.version = manifest.version; settings.userAgent = navigator.userAgent; @@ -340,7 +355,7 @@ function reportIssue(url, comment) { function authWavenet() { createTab("https://cloud.google.com/text-to-speech/#put-text-to-speech-into-action", true) - .then(function(tab) { + .then(function (tab) { addRequestListener(); brapi.tabs.onRemoved.addListener(onTabRemoved); return showInstructions(); @@ -361,14 +376,14 @@ function authWavenet() { var parser = new URL(details.url); var qs = parser.search ? parseQueryString(parser.search) : {}; if (qs.token) { - updateSettings({gcpToken: qs.token}); + updateSettings({ gcpToken: qs.token }); showSuccess(); } } function showInstructions() { return brapi.scripting.executeScript({ - target: {tabId: tab.id}, - func: function() { + target: { tabId: tab.id }, + func: function () { var elem = document.createElement('DIV') elem.id = 'ra-notice' elem.style.position = 'fixed' @@ -387,8 +402,8 @@ function authWavenet() { } function showSuccess() { return brapi.scripting.executeScript({ - target: {tabId: tab.id}, - func: function() { + target: { tabId: tab.id }, + func: function () { var elem = document.getElementById('ra-notice') elem.style.backgroundColor = '#0d0' elem.innerHTML = 'Successful, you can now use Google Wavenet voices. You may close this tab.' @@ -403,18 +418,18 @@ async function openPdfViewer(tabId, pdfUrl) { origins: ["http://*/", "https://*/"] } if (!await brapi.permissions.contains(perms)) { - throw new Error(JSON.stringify({code: "error_add_permissions", perms: perms})) + throw new Error(JSON.stringify({ code: "error_add_permissions", perms: perms })) } await setTabUrl(tabId, brapi.runtime.getURL("pdf-viewer.html?url=" + encodeURIComponent(pdfUrl))) await new Promise(f => handlers.pdfViewerCheckIn = f) } async function managePiperVoices() { - const result = await sendToPlayer({method: "managePiperVoices"}).catch(err => false) + const result = await sendToPlayer({ method: "managePiperVoices" }).catch(err => false) if (result != "OK") { - if (result == "POPOUT") await sendToPlayer({method: "close"}) + if (result == "POPOUT") await sendToPlayer({ method: "close" }) await injectPlayer() - await sendToPlayer({method: "managePiperVoices"}) + await sendToPlayer({ method: "managePiperVoices" }) } } @@ -426,7 +441,7 @@ async function contentScriptAlreadyInjected(tab, frameId) { tabId: tab.id, frameIds: frameId ? [frameId] : undefined, }, - func: function() { + func: function () { return typeof brapi != "undefined" } }) @@ -447,7 +462,7 @@ async function injectContentScript(tab, frameId, extraScripts) { "js/content.js", ] }) - const files = extraScripts || await brapi.tabs.sendMessage(tab.id, {dest: "contentScript", method: "getRequireJs"}) + const files = extraScripts || await brapi.tabs.sendMessage(tab.id, { dest: "contentScript", method: "getRequireJs" }) await brapi.scripting.executeScript({ target: { tabId: tab.id, @@ -468,7 +483,7 @@ async function injectPlayer(tab) { throw new Error("Incognito tab") } await brapi.scripting.executeScript({ - target: {tabId: tab.id}, + target: { tabId: tab.id }, func: createPlayerFrame }) } @@ -499,7 +514,7 @@ async function createPlayerTab() { index: 0, active: false, }) - await brapi.tabs.update(tab.id, {pinned: true}) + await brapi.tabs.update(tab.id, { pinned: true }) } diff --git a/js/player.js b/js/player.js index 16be0cc..6a5289e 100644 --- a/js/player.js +++ b/js/player.js @@ -8,23 +8,23 @@ var lastUrlPromise = Promise.resolve(null) const piperSubject = new rxjs.Subject() const piperObservable = rxjs.defer(() => { - createPiperFrame() - return piperSubject - }) + createPiperFrame() + return piperSubject +}) .pipe( - rxjs.shareReplay({bufferSize: 1, refCount: false}) + rxjs.shareReplay({ bufferSize: 1, refCount: false }) ) const piperCallbacks = new rxjs.Subject() const piperDispatcher = makeDispatcher("piper-host", { - advertiseVoices({voices}, sender) { - updateSettings({piperVoices: voices}) + advertiseVoices({ voices }, sender) { + updateSettings({ piperVoices: voices }) piperSubject.next(sender) }, - onStart: args => piperCallbacks.next({type: "start", ...args}), - onSentence: args => piperCallbacks.next({type: "sentence", ...args}), - onParagraph: args => piperCallbacks.next({type: "paragraph", ...args}), - onEnd: args => piperCallbacks.next({type: "end", ...args}), - onError: args => piperCallbacks.next({type: "error", ...args}), + onStart: args => piperCallbacks.next({ type: "start", ...args }), + onSentence: args => piperCallbacks.next({ type: "sentence", ...args }), + onParagraph: args => piperCallbacks.next({ type: "paragraph", ...args }), + onEnd: args => piperCallbacks.next({ type: "end", ...args }), + onError: args => piperCallbacks.next({ type: "error", ...args }), audioPlay: args => audioPlayer.play(args.src, args.rate, args.volume), audioPause: () => audioPlayer.pause(), audioResume: () => audioPlayer.resume(), @@ -40,7 +40,7 @@ const audioPlayer = immediate(() => { return new Promise((fulfill, reject) => { current = { playbackState$, - playback: playAudio(Promise.resolve(url), {rate, volume}, playbackState$).subscribe({ + playback: playAudio(Promise.resolve(url), { rate, volume }, playbackState$).subscribe({ complete: fulfill, error: reject }) @@ -59,12 +59,12 @@ const audioPlayer = immediate(() => { const fasttextSubject = new rxjs.Subject() const fasttextObservable = rxjs.defer(() => { - createFasttextFrame() - return fasttextSubject - }) + createFasttextFrame() + return fasttextSubject +}) .pipe( rxjs.startWith(null), - rxjs.shareReplay({bufferSize: 1, refCount: false}) + rxjs.shareReplay({ bufferSize: 1, refCount: false }) ) const fasttextDispatcher = makeDispatcher("fasttext-host", { onServiceReady(args, sender) { @@ -74,12 +74,12 @@ const fasttextDispatcher = makeDispatcher("fasttext-host", { window.addEventListener("message", event => { - const send = message => event.source.postMessage(message, {targetOrigin: event.origin}) + const send = message => event.source.postMessage(message, { targetOrigin: event.origin }) piperDispatcher.dispatch(event.data, { sendRequest(method, args) { const id = String(Math.random()) - send({from: "piper-host", to: "piper-service", type: "request", id, method, args}) + send({ from: "piper-host", to: "piper-service", type: "request", id, method, args }) return piperDispatcher.waitForResponse(id) } }, send) @@ -87,7 +87,7 @@ window.addEventListener("message", event => { fasttextDispatcher.dispatch(event.data, { sendRequest(method, args) { const id = String(Math.random()) - send({from: "fasttext-host", to: "fasttext-service", type: "request", id, method, args}) + send({ from: "fasttext-host", to: "fasttext-service", type: "request", id, method, args }) return fasttextDispatcher.waitForResponse(id) } }, send) @@ -101,7 +101,7 @@ if (queryString.has("autoclose")) rxjs.combineLatest(idleSubject, piperSubject.pipe(rxjs.startWith(null))) .pipe( rxjs.switchMap(([isIdle, piper]) => { - if (isIdle) return rxjs.timer(queryString.get("autoclose") == "long" || piper ? 15*60*1000 : 5*60*1000) + if (isIdle) return rxjs.timer(queryString.get("autoclose") == "long" || piper ? 15 * 60 * 1000 : 5 * 60 * 1000) else return rxjs.EMPTY }) ) @@ -125,12 +125,13 @@ var messageHandlers = { isPaired: () => phoneTtsEngine.isPaired(), managePiperVoices, getLastUrl: () => lastUrlPromise, + synthesizeDownloadAudio: synthesizeDownloadAudio, } registerMessageListener("player", messageHandlers) if (queryString.has("opener")) { - brapi.runtime.sendMessage({dest: queryString.get("opener"), method: "playerCheckIn"}) + brapi.runtime.sendMessage({ dest: queryString.get("opener"), method: "playerCheckIn" }) .catch(console.error) } else { bgPageInvoke("playerCheckIn") @@ -139,22 +140,206 @@ if (queryString.has("opener")) { document.addEventListener("DOMContentLoaded", initialize) +async function synthesizeDownloadAudio(text) { + console.log("PLAYER: synthesizeDownloadAudio received:", text); + + // Load settings + const settings = await getSettings(["voiceName", "rate", "pitch", "voices"]); + + // Resolve voice list + let voiceList = settings.voices || []; + if (!voiceList.length) { + // fallback: detect voices from engines (GoogleTranslate) + voiceList = googleTranslateTtsEngine.getVoices(); + } + + // Resolve active voice + const voiceName = + settings.voiceName || + (voiceList[0] && voiceList[0].voiceName); + + let voice = voiceList.find(v => v.voiceName === voiceName); + + if (!voice) { + console.error("Voice detection failed. settings.voiceName =", settings.voiceName); + throw new Error("Unable to detect current playback voice"); + } + + console.log("Voice detected for DOWNLOAD:", voice); + + // if GoogleTranslate voice → force English Wavenet voice + if (voice.voiceName.startsWith("GoogleTranslate")) { + console.warn("GoogleTranslate voice detected — forcing Wavenet"); + + // Load Wavenet voices properly (it's async) + let wavenetList = []; + try { + wavenetList = await googleWavenetTtsEngine.getVoices(); + } catch (e) { + console.error("Failed to load Wavenet voices:", e); + } + + console.log("Loaded Wavenet list:", wavenetList); + + // Pick English voice + const fallbackVoice = wavenetList.find(v => v.lang && v.lang.startsWith("en")) || null; + + console.log("FallbackVoice: ", fallbackVoice); + + if (!fallbackVoice) { + throw new Error("No English Wavenet voices available."); + } + + // override actual voice + voice = fallbackVoice; + + } + + const options = { + lang: voice.lang, + voice: voice, + rate: settings.rate || 1.0, + pitch: settings.pitch || 1.0, + }; + + console.log(" Step 1 — worked"); + + // Create Speech instance + const speech = new Speech([text], options); + + console.log(" Step 2 — worked"); + + const engine = speech.engine || speech._engine || speech.__engine; + if (!engine) throw new Error("Cannot access speech engine"); + + console.log("Using engine for DOWNLOAD:", engine.constructor.name); + console.log(" Step 3 — worked"); + + // Get audio URL + let url; + + if (engine.getAudioUrl) { + url = await engine.getAudioUrl(text, options.voice, options.pitch); + } else { + url = await getAudioUrlForDownload(engine, text, options); + } + + if (!url) throw new Error("Unable to obtain audio URL"); + console.log(" Step 4 — worked"); + + // Fetch blob + const blob = await fetch(url).then(r => r.blob()); + + console.log(" Step 5 — worked"); + + // Convert to base64 + return await new Promise(resolve => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(blob); + }); +} + + +async function getAudioUrlForDownload(engine, text, options) { + + // Case 1: Engine already has getAudioUrl() + if (engine.getAudioUrl) { + return await engine.getAudioUrl(text, options.voice, options.pitch); + } + + // Case 2: Google Translate engine + if (engine.constructor.name === "GoogleTranslateTtsEngine") { + throw new Error("GoogleTranslate engine cannot be used for download"); + } + + // Case 3: OpenAI engine + if (engine.constructor.name === "OpenaiTtsEngine") { + return await engine._getAudioUrl(text, options.voice, options.pitch); + } + + // Case 4: If engine starts playback using playAudio(url) + if (engine.speak) { + // Patch speak() to capture the URL + return new Promise(async (resolve, reject) => { + const playback = engine.speak(text, options, new rxjs.BehaviorSubject("resumed")); + playback.subscribe({ + next(event) { + if (event.type === "start" && event.src) { + resolve(event.src); // capture URL + } + }, + error: reject, + complete: () => reject("No URL found") + }); + }); + } + + throw new Error("Engine does not support audio download"); +} + + +async function synthesizeBlob(text) { + const opts = await getSettings(["voiceName", "rate", "pitch"]) + .then(x => ({ + voice: { voiceName: x.voiceName }, + rate: x.rate, + pitch: x.pitch, + })); + + // Create temporary Speech instance + const speech = new Speech([text], opts); + + // Instead of createAudioBlob(), use the engine directly: + const engine = speech.engine; // internal engine chosen by pickEngine() + + if (!engine.getAudioUrl) { + throw new Error("Selected voice engine does not support audio download."); + } + + // Get raw URL + const url = await engine.getAudioUrl(text, opts.voice, opts.pitch); + + // Convert URL to Blob + const res = await fetch(url); + const blob = await res.blob(); + + return blob; +} + + +function blobToBase64(blob) { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(blob); + }); +} + +async function synthesizeSpeech(text, options) { + // Use the same engine the player normally uses + const speech = new Speech([text], options); + + // call internal engine to produce audio Blob + const audio = await speech.createAudioBlob(); + return audio; +} async function initialize() { setI18nText() $("#hidethistab-link") .toggle(canUseEmbeddedPlayer() && !(await getSettings()).useEmbeddedPlayer) - .click(function() { + .click(function () { $("#dialog-backdrop, #hidethistab-dialog").show() }) $("#hidethistab-dialog .btn, #hidethistab-dialog .close") - .click(function(event) { + .click(function (event) { $("#dialog-backdrop, #hidethistab-dialog").hide() if ($(event.target).is(".btn-ok")) { - updateSettings({useEmbeddedPlayer: true}) + updateSettings({ useEmbeddedPlayer: true }) .then(() => window.close()) .catch(console.error) } @@ -165,13 +350,13 @@ function playText(text, opts) { opts = opts || {} playbackError = null if (!activeDoc) { - openDoc(new SimpleSource(text.split(/(?:\r?\n){2,}/), {lang: opts.lang}), function(err) { + openDoc(new SimpleSource(text.split(/(?:\r?\n){2,}/), { lang: opts.lang }), function (err) { if (err) playbackError = err }) } const doc = activeDoc return activeDoc.play() - .catch(function(err) { + .catch(function (err) { if (doc == activeDoc) { handleError(err); closeDoc(); @@ -183,13 +368,13 @@ function playText(text, opts) { function playTab() { playbackError = null if (!activeDoc) { - openDoc(new TabSource(), function(err) { + openDoc(new TabSource(), function (err) { if (err) playbackError = err }) } const doc = activeDoc return activeDoc.play() - .catch(function(err) { + .catch(function (err) { if (doc == activeDoc) { handleError(err); closeDoc(); @@ -219,7 +404,7 @@ function resume() { function getPlaybackState() { if (activeDoc) { return Promise.all([activeDoc.getState(), activeDoc.getActiveSpeech()]) - .then(function(results) { + .then(function (results) { return { state: results[0], speechInfo: results[1] && results[1].getInfo(), @@ -239,7 +424,7 @@ function getPlaybackState() { } function openDoc(source, onEnd) { - activeDoc = new Doc(source, function(err) { + activeDoc = new Doc(source, function (err) { handleError(err); closeDoc(); if (typeof onEnd == "function") onEnd(err); @@ -304,14 +489,14 @@ function playAudio(urlPromise, options, playbackState$) { } } -var requestAudioPlaybackPermission = lazy(async function() { +var requestAudioPlaybackPermission = lazy(async function () { const thisTab = await brapi.tabs.getCurrent() - const prevTab = await brapi.tabs.query({windowId: thisTab.windowId, active: true}).then(tabs => tabs[0]) - await brapi.tabs.update(thisTab.id, {active: true}) + const prevTab = await brapi.tabs.query({ windowId: thisTab.windowId, active: true }).then(tabs => tabs[0]) + await brapi.tabs.update(thisTab.id, { active: true }) $("#dialog-backdrop, #audio-playback-permission-dialog").show() await new Audio(brapi.runtime.getURL("sound/silence.mp3")).play() $("#dialog-backdrop, #audio-playback-permission-dialog").hide() - await brapi.tabs.update(prevTab.id, {active: true}) + await brapi.tabs.update(prevTab.id, { active: true }) }) async function createOffscreen() { @@ -334,10 +519,10 @@ function playAudioOffscreen(urlPromise, options, playbackState$) { if (state == "resumed") { return rxjs.defer(async () => { if (!playback$) { - const result = await sendToOffscreen({method: "play", args: [url, options]}) + const result = await sendToOffscreen({ method: "play", args: [url, options] }) if (result != true) throw "Offscreen doc not present" } else { - const result = await sendToOffscreen({method: "resume"}) + const result = await sendToOffscreen({ method: "resume" }) if (result != true) throw "Offscreen doc gone" } }).pipe( @@ -345,7 +530,7 @@ function playAudioOffscreen(urlPromise, options, playbackState$) { console.debug(err) return rxjs.defer(createOffscreen).pipe( rxjs.exhaustMap(async () => { - const result = await sendToOffscreen({method: "play", args: [url, options]}) + const result = await sendToOffscreen({ method: "play", args: [url, options] }) if (result != true) throw new Error("Offscreen doc inaccessible") }) ) @@ -353,7 +538,7 @@ function playAudioOffscreen(urlPromise, options, playbackState$) { rxjs.exhaustMap(() => rxjs.NEVER.pipe( rxjs.finalize(() => { - sendToOffscreen({method: "pause"}) + sendToOffscreen({ method: "pause" }) .catch(console.error) }) ) @@ -368,7 +553,7 @@ function playAudioOffscreen(urlPromise, options, playbackState$) { ), rxjs.mergeWith( new rxjs.Observable(observer => { - messageHandlers.offscreenPlaybackEvent = function(event) { + messageHandlers.offscreenPlaybackEvent = function (event) { if (event.type == "error") observer.error(event.error) else observer.next(event) } @@ -418,8 +603,8 @@ function managePiperVoices() { .catch(console.error) brapi.tabs.getCurrent() .then(tab => Promise.all([ - brapi.windows.update(tab.windowId, {focused: true}), - brapi.tabs.update(tab.id, {active: true}) + brapi.windows.update(tab.windowId, { focused: true }), + brapi.tabs.update(tab.id, { active: true }) ])) .catch(console.error) return "OK" @@ -433,9 +618,9 @@ function createPiperFrame() { f.allow = "cross-origin-isolated" f.style.position = "absolute" f.style.left = - f.style.top = "0" + f.style.top = "0" f.style.width = - f.style.height = "100%" + f.style.height = "100%" f.style.borderWidth = "0" document.body.appendChild(f) } diff --git a/js/popup.js b/js/popup.js index e8442d2..a5beaa5 100644 --- a/js/popup.js +++ b/js/popup.js @@ -320,20 +320,30 @@ function changeFontSize(delta) { async function onDownload() { try { - const text = await bgPageInvoke("getSelectedTextFromTab"); + // 1. Ask BACKGROUND for selected text + const selectedText = await bgPageInvoke("getSelectedText"); - if (!text) return alert("Please select some text first."); + if (!selectedText) { + alert("Please select some text first."); + return; + } + + console.log("Popup received selected text:", selectedText); - const base64Audio = await bgPageInvoke("downloadSelectedText", text); + // 2. Ask BACKGROUND to synthesize audio + const base64Audio = await bgPageInvoke("downloadSelectedText", [selectedText]); + // 3. Trigger file download const a = document.createElement("a"); a.href = base64Audio; a.download = "readaloud.mp3"; a.click(); + console.log("Audio download completed"); + } catch (err) { - console.error(err); - alert("Failed to download audio."); + console.error("DOWNLOAD ERROR → FULL ERROR:", JSON.stringify(err, null, 2)); + alert("Unable to download file. Check console."); } } diff --git a/js/speech.js b/js/speech.js index 34477e0..0bf8aeb 100644 --- a/js/speech.js +++ b/js/speech.js @@ -7,6 +7,7 @@ function Speech(texts, options) { var self = this; const engine = pickEngine() + this.engine = engine; let piperState this.options = options; From fbd492a856e77172fb24edf162de8dc59abe45e2 Mon Sep 17 00:00:00 2001 From: Dnyaneshwar Bhajantri Date: Tue, 2 Dec 2025 12:50:25 +0530 Subject: [PATCH 3/3] removed the consoles --- js/download.js | 9 ---- js/player.js | 111 +++++++++++++------------------------------------ js/popup.js | 92 ++++++++++++++++++++-------------------- 3 files changed, 77 insertions(+), 135 deletions(-) delete mode 100644 js/download.js diff --git a/js/download.js b/js/download.js deleted file mode 100644 index d9bd8a4..0000000 --- a/js/download.js +++ /dev/null @@ -1,9 +0,0 @@ -// brapi.runtime.onMessage.addListener((msg, sender) => { -// console.log("In downloads the message is received", msg); -// if (msg.action === "downloadSelectedText") { -// console.log("Received selected text:", msg.text); - -// // next step: convert to audio -// startDownloadProcess(msg.text); -// } -// }); diff --git a/js/player.js b/js/player.js index 6a5289e..fb8bef0 100644 --- a/js/player.js +++ b/js/player.js @@ -141,107 +141,54 @@ if (queryString.has("opener")) { document.addEventListener("DOMContentLoaded", initialize) async function synthesizeDownloadAudio(text) { - console.log("PLAYER: synthesizeDownloadAudio received:", text); - // Load settings - const settings = await getSettings(["voiceName", "rate", "pitch", "voices"]); + // SETTINGS + const settings = await getSettings(["rate", "pitch"]); + const lang = "en"; // best voice - // Resolve voice list - let voiceList = settings.voices || []; - if (!voiceList.length) { - // fallback: detect voices from engines (GoogleTranslate) - voiceList = googleTranslateTtsEngine.getVoices(); - } - - // Resolve active voice - const voiceName = - settings.voiceName || - (voiceList[0] && voiceList[0].voiceName); + // SPLIT TEXT INTO SMALL CHUNKS (MAX 180 characters) + const chunks = []; + let remaining = text.trim(); - let voice = voiceList.find(v => v.voiceName === voiceName); - - if (!voice) { - console.error("Voice detection failed. settings.voiceName =", settings.voiceName); - throw new Error("Unable to detect current playback voice"); + while (remaining.length > 0) { + chunks.push(remaining.slice(0, 180)); + remaining = remaining.slice(180); } - console.log("Voice detected for DOWNLOAD:", voice); + const audioBlobs = []; - // if GoogleTranslate voice → force English Wavenet voice - if (voice.voiceName.startsWith("GoogleTranslate")) { - console.warn("GoogleTranslate voice detected — forcing Wavenet"); + // STEP 3 — GET AUDIO URL FOR EACH CHUNK + for (let i = 0; i < chunks.length; i++) { + const part = chunks[i]; - // Load Wavenet voices properly (it's async) - let wavenetList = []; + let url; try { - wavenetList = await googleWavenetTtsEngine.getVoices(); - } catch (e) { - console.error("Failed to load Wavenet voices:", e); + url = await googleTranslateSynthesizeSpeech(part, lang); + } catch (err) { + console.error("Chunk synthesis failed:", err); + throw new Error("GoogleTranslate TTS failed for chunk " + (i + 1)); } - console.log("Loaded Wavenet list:", wavenetList); - - // Pick English voice - const fallbackVoice = wavenetList.find(v => v.lang && v.lang.startsWith("en")) || null; - - console.log("FallbackVoice: ", fallbackVoice); - - if (!fallbackVoice) { - throw new Error("No English Wavenet voices available."); - } - - // override actual voice - voice = fallbackVoice; - - } - - const options = { - lang: voice.lang, - voice: voice, - rate: settings.rate || 1.0, - pitch: settings.pitch || 1.0, - }; - - console.log(" Step 1 — worked"); - - // Create Speech instance - const speech = new Speech([text], options); - - console.log(" Step 2 — worked"); - - const engine = speech.engine || speech._engine || speech.__engine; - if (!engine) throw new Error("Cannot access speech engine"); - - console.log("Using engine for DOWNLOAD:", engine.constructor.name); - console.log(" Step 3 — worked"); - - // Get audio URL - let url; - - if (engine.getAudioUrl) { - url = await engine.getAudioUrl(text, options.voice, options.pitch); - } else { - url = await getAudioUrlForDownload(engine, text, options); + // FETCH BLOB + const blob = await fetch(url).then(r => r.blob()); + audioBlobs.push(blob); } - if (!url) throw new Error("Unable to obtain audio URL"); + // MERGE ALL BLOBS INTO ONE FINAL MP3 + const merged = new Blob(audioBlobs, { type: "audio/mp3" }); - console.log(" Step 4 — worked"); - - // Fetch blob - const blob = await fetch(url).then(r => r.blob()); - - console.log(" Step 5 — worked"); - - // Convert to base64 + // STEP 5 — Convert to Base64 return await new Promise(resolve => { const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.readAsDataURL(blob); + reader.onloadend = () => { + resolve(reader.result); + }; + reader.readAsDataURL(merged); }); } + async function getAudioUrlForDownload(engine, text, options) { // Case 1: Engine already has getAudioUrl() diff --git a/js/popup.js b/js/popup.js index a5beaa5..f682189 100644 --- a/js/popup.js +++ b/js/popup.js @@ -18,9 +18,9 @@ piperInitializingSubject else $("#status").hide() }) -$(function() { +$(function () { if (queryString.isPopup) $("body").addClass("is-popup") - else getCurrentTab().then(function(currentTab) {return updateSettings({readAloudTab: currentTab.id})}) + else getCurrentTab().then(function (currentTab) { return updateSettings({ readAloudTab: currentTab.id }) }) }) @@ -38,8 +38,8 @@ async function popout(tabId) { const url = brapi.runtime.getURL("popup.html?tab=" + activeTab.id) try { if (!tabId) throw "Create" - const tab = await updateTab(tabId, {url, active: true}) - await updateWindow(tab.windowId, {focused: true}).catch(console.error) + const tab = await updateTab(tabId, { url, active: true }) + await updateWindow(tab.windowId, { focused: true }).catch(console.error) window.close() } catch (err) { @@ -72,7 +72,7 @@ async function init() { refreshSize(); checkAnnouncements(); - const {state} = await bgPageInvoke("getPlaybackState") + const { state } = await bgPageInvoke("getPlaybackState") if (state == "PAUSED" || state == "STOPPED") onPlay() } @@ -84,14 +84,14 @@ function handleError(err) { var errInfo = JSON.parse(err.message); $("#status").html(formatError(errInfo)).show(); - $("#status a").click(function() { + $("#status a").click(function () { switch ($(this).attr("href")) { case "#open-extension-settings": - brapi.tabs.create({url: "chrome://extensions/?id=" + brapi.runtime.id}); + brapi.tabs.create({ url: "chrome://extensions/?id=" + brapi.runtime.id }); break; case "#request-permissions": brapi.permissions.request(errInfo.perms) - .then(function(granted) { + .then(function (granted) { if (granted) { if (errInfo.reload) return reloadAndPlay() else $("#btnPlay").click() @@ -99,22 +99,22 @@ function handleError(err) { }) break; case "#sign-in": - getAuthToken({interactive: true}) - .then(function(token) { + getAuthToken({ interactive: true }) + .then(function (token) { if (token) $("#btnPlay").click(); }) - .catch(function(err) { + .catch(function (err) { $("#status").text(err.message).show(); }) break; case "#auth-wavenet": brapi.permissions.request(config.wavenetPerms) - .then(function(granted) { + .then(function (granted) { if (granted) bgPageInvoke("authWavenet"); }) break; case "#open-pdf-viewer": - brapi.tabs.create({url: config.pdfViewerUrl}) + brapi.tabs.create({ url: config.pdfViewerUrl }) break case "#connect-phone": location.href = "connect-phone.html" @@ -124,7 +124,7 @@ function handleError(err) { } else if (config.browserId == "opera" && /locked fullscreen/.test(err.message)) { $("#status").html("Click here to start read aloud.").show() - $("#status a").click(async function() { + $("#status a").click(async function () { try { playerCheckIn$.pipe(rxjs.take(1)).subscribe(() => $("#btnPlay").click()) const tab = await brapi.tabs.create({ @@ -132,7 +132,7 @@ function handleError(err) { index: 0, active: false, }) - brapi.tabs.update(tab.id, {pinned: true}) + brapi.tabs.update(tab.id, { pinned: true }) .catch(console.error) } catch (err) { handleError(err) @@ -181,12 +181,12 @@ function updateHighlighting(speech) { var elem = $("#highlight"); if (!elem.data("texts") || elem.data("texts").length != speech.texts.length - || elem.data("texts").some((text,i) => text != speech.texts[i]) + || elem.data("texts").some((text, i) => text != speech.texts[i]) ) { elem.css("direction", speech.isRTL ? "rtl" : "") - .data({texts: speech.texts, position: null}) + .data({ texts: speech.texts, position: null }) .empty() - for (let i=0; i= scrollParent.height()) - scrollParent.animate({scrollTop: scrollParent[0].scrollTop + childTop - 10}) + scrollParent.animate({ scrollTop: scrollParent[0].scrollTop + childTop - 10 }) } @@ -258,7 +258,7 @@ function onPlay() { $("#status").hide(); const requestId = currentPlayRequestId = Math.random() bgPageInvoke("getPlaybackState") - .then(function(stateInfo) { + .then(function (stateInfo) { if (stateInfo.state == "PAUSED") return bgPageInvoke("resume") else return bgPageInvoke("playTab", queryString.tab ? [Number(queryString.tab)] : []) }) @@ -311,16 +311,16 @@ function onSeek(n) { function changeFontSize(delta) { getSettings(["highlightFontSize"]) - .then(function(settings) { + .then(function (settings) { var newSize = (settings.highlightFontSize || defaults.highlightFontSize) + delta; - if (newSize >= 1 && newSize <= 8) return updateSettings({highlightFontSize: newSize}).then(refreshSize); + if (newSize >= 1 && newSize <= 8) return updateSettings({ highlightFontSize: newSize }).then(refreshSize); }) .catch(handleError) } async function onDownload() { try { - // 1. Ask BACKGROUND for selected text + // Ask BACKGROUND for selected text const selectedText = await bgPageInvoke("getSelectedText"); if (!selectedText) { @@ -328,38 +328,42 @@ async function onDownload() { return; } - console.log("Popup received selected text:", selectedText); - - // 2. Ask BACKGROUND to synthesize audio + // Ask BACKGROUND to synthesize audio const base64Audio = await bgPageInvoke("downloadSelectedText", [selectedText]); - // 3. Trigger file download + // Trigger file download const a = document.createElement("a"); a.href = base64Audio; - a.download = "readaloud.mp3"; + a.download = makeSafeFilename(selectedText) + ".mp3"; a.click(); - console.log("Audio download completed"); - } catch (err) { console.error("DOWNLOAD ERROR → FULL ERROR:", JSON.stringify(err, null, 2)); - alert("Unable to download file. Check console."); + alert("Unable to download file. Check console."); } } +function makeSafeFilename(text) { + return text + .trim() + .slice(0, 40) // limit length + .replace(/[\/\\:*?"<>|]/g, "") // remove illegal characters + .replace(/\s+/g, "_") // replace spaces + .toLowerCase(); +} function changeWindowSize(delta) { getSettings(["highlightWindowSize"]) - .then(function(settings) { + .then(function (settings) { var newSize = (settings.highlightWindowSize || defaults.highlightWindowSize) + delta; - if (newSize >= 1 && newSize <= 3) return updateSettings({highlightWindowSize: newSize}).then(refreshSize); + if (newSize >= 1 && newSize <= 3) return updateSettings({ highlightWindowSize: newSize }).then(refreshSize); }) .catch(handleError) } function refreshSize() { return getSettings(["highlightFontSize", "highlightWindowSize"]) - .then(function(settings) { + .then(function (settings) { var fontSize = getFontSize(settings); var windowSize = getWindowSize(settings); $("#highlight").css({ @@ -394,29 +398,29 @@ function refreshSize() { function checkAnnouncements() { var now = new Date().getTime(); getSettings(["announcement"]) - .then(function(settings) { + .then(function (settings) { var ann = settings.announcement; if (ann && ann.expire > now) return ann; else return ajaxGet(config.serviceUrl + "/read-aloud/announcement") .then(JSON.parse) - .then(function(result) { - result.expire = now + 6*3600*1000; + .then(function (result) { + result.expire = now + 6 * 3600 * 1000; if (ann && result.id == ann.id) { result.lastShown = ann.lastShown; result.disabled = ann.disabled; } - updateSettings({announcement: result}); + updateSettings({ announcement: result }); return result; }) }) - .then(function(ann) { + .then(function (ann) { if (ann.text && !ann.disabled) { - if (!ann.lastShown || now-ann.lastShown > ann.period*60*1000) { + if (!ann.lastShown || now - ann.lastShown > ann.period * 60 * 1000) { showAnnouncement(ann); ann.lastShown = now; - updateSettings({announcement: ann}); + updateSettings({ announcement: ann }); } } }) @@ -426,13 +430,13 @@ function showAnnouncement(ann) { var html = escapeHtml(ann.text).replace(/\[(.*?)\]/g, "$1").replace(/\n/g, "
"); $("#footer").html(html).addClass("announcement"); if (ann.disableIfClick) - $("#footer a").click(function() { + $("#footer a").click(function () { ann.disabled = true; - updateSettings({announcement: ann}); + updateSettings({ announcement: ann }); }) } function toggleDarkMode() { const darkMode = document.body.classList.toggle("dark-mode") - updateSettings({darkMode}) + updateSettings({ darkMode }) }