Read highlighted text with speech synthesis
// ==UserScript==
// @name Read the section highlight (Transparent Menu)
// @namespace http://tampermonkey.net/
// @version 2.6
// @license MIT
// @description Read highlighted text with speech synthesis
// @author an vu an
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const menu = document.createElement("div");
menu.style.position = "fixed";
menu.style.bottom = "20px";
menu.style.right = "20px";
menu.style.zIndex = "9999";
menu.style.background = "transparent";
menu.style.border = "none";
menu.style.borderRadius = "0";
menu.style.padding = "0";
menu.style.boxShadow = "none";
menu.style.fontSize = "14px";
menu.style.fontFamily = "sans-serif";
document.body.appendChild(menu);
menu.innerHTML = `
<div style="margin-bottom:6px; font-weight:bold; color:#ff6600;">🔊 Read</div>
<button id="speakBtn" style="background:transparent; border:none; color:blue; cursor:pointer;">▶️ Read 1 time</button><br>
<button id="speakLoopBtn" style="background:transparent; border:none; color:green; cursor:pointer;">🔁 Read continuously</button><br>
<button id="stopBtn" style="background:transparent; border:none; color:red; cursor:pointer;">⏹ Stop</button>
`;
const speakBtn = menu.querySelector("#speakBtn");
const speakLoopBtn = menu.querySelector("#speakLoopBtn");
const stopBtn = menu.querySelector("#stopBtn");
let selectedVoice = null;
let loopMode = false;
function loadVoices() {
const voices = speechSynthesis.getVoices();
const vi = voices.find(v => v.lang.startsWith("vi"));
if (vi) {
selectedVoice = vi;
} else {
selectedVoice = voices[0]; // fallback
}
}
window.speechSynthesis.onvoiceschanged = loadVoices;
loadVoices();
function speakText(text) {
const synth = window.speechSynthesis;
synth.cancel();
text = text.replace(/[^\p{L}\p{N}\s.,!?]/gu, "");
if (!text) {
alert("❗ No valid content to read");
return;
}
const utter = new SpeechSynthesisUtterance(text);
if (selectedVoice) {
utter.voice = selectedVoice;
utter.lang = selectedVoice.lang;
} else {
utter.lang = "vi-VN";
}
utter.onend = () => {
if (loopMode) speakText(text);
};
synth.speak(utter);
}
speakBtn.addEventListener("click", () => {
loopMode = false;
const text = window.getSelection().toString().trim();
if (!text) {
alert("❗ Hãy bôi xanh văn bản để đọc");
return;
}
speakText(text);
});
speakLoopBtn.addEventListener("click", () => {
loopMode = true;
const text = window.getSelection().toString().trim();
if (!text) {
alert("❗ Highlight text to read");
return;
}
speakText(text);
});
stopBtn.addEventListener("click", () => {
loopMode = false;
window.speechSynthesis.cancel();
});
})();