您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Легковесный скрипт для чтения вслух текста со страниц. Чтение с места клика, регулировка скорости, базовый скроллинг.
// ==UserScript== // @name Простой Ридер Текста // @namespace http://tampermonkey.net/ // @version 4.0 // @description Легковесный скрипт для чтения вслух текста со страниц. Чтение с места клика, регулировка скорости, базовый скроллинг. // @author You // @match *://*/* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // Функция для создания панели поддержки function createSupportPanel() { if (document.getElementById('ttsSupportPanel')) { return; } const supportPanel = document.createElement('div'); supportPanel.id = 'ttsSupportPanel'; supportPanel.innerHTML = ` <div style="text-align: center; line-height: 1.3;"> Если вам нравится скрипт, вы можете сделать добровольный взнос:<br> <a href="https://finance.ozon.ru/apps/sbp/ozonbankpay/01997254-f6d5-7d4c-8a72-15f9a62fea9d" target="_blank" style="color: #0066cc; text-decoration: none; font-weight: bold;"> Поддержать проект </a> </div> `; Object.assign(supportPanel.style, { position: 'fixed', bottom: '10px', right: '10px', background: 'rgba(255, 255, 255, 0.95)', border: '1px solid #ddd', borderRadius: '8px', padding: '8px 12px', fontSize: '11px', fontFamily: 'Arial, sans-serif', color: '#666', zIndex: '9999', maxWidth: '200px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)', backdropFilter: 'blur(5px)', opacity: '0.8', transition: 'opacity 0.3s ease' }); supportPanel.addEventListener('mouseenter', () => { supportPanel.style.opacity = '1'; }); supportPanel.addEventListener('mouseleave', () => { supportPanel.style.opacity = '0.8'; }); try { document.body.appendChild(supportPanel); } catch (e) { document.documentElement.appendChild(supportPanel); } return supportPanel; } // Функция для безопасного добавления панели function createControlPanel() { if (document.getElementById('ttsControlPanel')) { return; } const controlPanel = document.createElement('div'); controlPanel.id = 'ttsControlPanel'; controlPanel.innerHTML = ` <div style="padding: 5px; cursor: move; border-bottom: 1px solid #ccc; background: #f0f0f0; border-radius: 5px 5px 0 0; font-size: 12px;">📢 Ридер Текста</div> <div style="padding: 5px;"> <button id="ttsPlay" style="margin: 2px; padding: 5px;">▶️</button> <button id="ttsPause" style="margin: 2px; padding: 5px;">⏸️</button> <button id="ttsStop" style="margin: 2px; padding: 5px;">⏹️</button> <button id="ttsFaster" style="margin: 2px; padding: 5px; font-size: 10px;">⏩ +</button> <button id="ttsSlower" style="margin: 2px; padding: 5px; font-size: 10px;">⏪ -</button> <div style="margin-top: 5px; font-size: 11px; text-align: center;"> Скорость: <span id="speedValue">1.0</span>x </div> <button id="ttsResetSpeed" style="margin: 2px; padding: 3px; font-size: 9px; width: 100%;">Сбросить скорость</button> </div> <div style="padding: 5px; background: #f9f9f9; border-top: 1px solid #eee; font-size: 9px; text-align: center; color: #666;"> Двойной клик по тексту для чтения с места </div> `; Object.assign(controlPanel.style, { position: 'fixed', top: '20px', right: '20px', background: 'white', border: '1px solid #ccc', borderRadius: '5px', zIndex: '10000', boxShadow: '0 2px 10px rgba(0,0,0,0.1)', userSelect: 'none', minWidth: '140px', fontFamily: 'Arial, sans-serif' }); document.body.appendChild(controlPanel); return controlPanel; } // Функции для работы с localStorage function saveSettings() { try { localStorage.setItem('ttsReadingSpeed', currentRate.toString()); } catch (e) { console.log('Не удалось сохранить настройки:', e); } } function loadSettings() { try { const savedSpeed = localStorage.getItem('ttsReadingSpeed'); if (savedSpeed) { const speed = parseFloat(savedSpeed); if (speed >= 0.5 && speed <= 3.0) { currentRate = speed; } } } catch (e) { console.log('Не удалось загрузить настройки:', e); } } // Ждем полной загрузки DOM if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Переменные для управления речью let speechInstance = null; let isPaused = false; let currentRate = 1.0; let currentText = ''; let isManualStop = false; let wordBoundaries = []; let currentWordIndex = 0; let speechStartTime = 0; let estimatedPosition = 0; function init() { setTimeout(() => { const controlPanel = createControlPanel(); const supportPanel = createSupportPanel(); if (controlPanel) { loadSettings(); setupEventListeners(); setupDoubleClickHandler(); makePanelDraggable(controlPanel); updateSpeedDisplay(); } }, 1000); } // Функция для настройки обработчика двойного клика function setupDoubleClickHandler() { document.addEventListener('dblclick', handleDoubleClick); } // Обработчик двойного клика function handleDoubleClick(event) { if (event.target.closest('#ttsControlPanel') || event.target.closest('#ttsSupportPanel')) { return; } const selection = window.getSelection(); let targetElement = event.target; if (selection.toString().trim().length > 0) { const range = selection.getRangeAt(0); targetElement = range.commonAncestorContainer; if (targetElement.nodeType === Node.TEXT_NODE) { targetElement = targetElement.parentElement; } } const startPosition = findTextPosition(targetElement); if (startPosition !== -1) { playFromPosition(startPosition); } } // Находим позицию в тексте для начала чтения function findTextPosition(element) { if (!currentText) { currentText = extractMainText(); if (!currentText) { alert('Сначала нажмите кнопку воспроизведения для извлечения текста'); return -1; } wordBoundaries = calculateWordBoundaries(currentText); } const elementText = element.textContent || ''; if (!elementText.trim()) return -1; const elementTextClean = cleanTextSimple(elementText); const position = currentText.indexOf(elementTextClean); if (position === -1) { const words = elementTextClean.split(/\s+/).filter(w => w.length > 2); if (words.length > 0) { for (let word of words) { const wordPosition = currentText.indexOf(word); if (wordPosition !== -1) { return wordPosition; } } } return -1; } return position; } // Функция для обновления отображения скорости function updateSpeedDisplay() { const speedElement = document.getElementById('speedValue'); if (speedElement) { speedElement.textContent = currentRate.toFixed(1); if (currentRate > 1.0) { speedElement.style.color = '#e74c3c'; } else if (currentRate < 1.0) { speedElement.style.color = '#3498db'; } else { speedElement.style.color = '#2c3e50'; } } } // ПРОСТАЯ функция для извлечения текста function extractMainText() { // Стратегия 1: Ищем по специфичным селекторам const selectors = [ 'article', 'main', '[role="main"]', '.article', '.story', '.news', '.content', '.post-content', '.article-content', '.news-content', '.entry-content', '.story-content', '.text-content', '.text' ]; for (let selector of selectors) { const elements = document.querySelectorAll(selector); for (let element of elements) { if (isValidContentElement(element)) { const text = extractCleanText(element); if (text && text.length > 200) { return text; } } } } // Стратегия 2: Ищем самый большой текстовый блок const elements = document.querySelectorAll('p, div, section, article, main'); let bestElement = null; let maxLength = 0; for (let element of elements) { if (isValidContentElement(element)) { const text = extractCleanText(element); if (text && text.length > maxLength) { maxLength = text.length; bestElement = element; } } } if (bestElement && maxLength > 300) { return extractCleanText(bestElement); } return null; } // Извлекаем чистый текст из элемента function extractCleanText(element) { const clone = element.cloneNode(true); removeUnwantedElements(clone); const paragraphs = clone.querySelectorAll('p, h1, h2, h3, h4, h5, h6'); const textParts = []; for (let p of paragraphs) { const text = cleanTextSimple(p.textContent); if (text && text.length > 20) { textParts.push(text); } } return textParts.length > 0 ? textParts.join('\n\n') : cleanTextSimple(clone.textContent); } // УПРОЩЕННАЯ очистка текста function cleanTextSimple(text) { if (!text) return ''; return text .replace(/https?:\/\/[^\s]+/g, '') .replace(/www\.[^\s]+/g, '') .replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '') .replace(/\b[a-zA-Z]{1,2}\b/g, '') .replace(/[^\w\u0400-\u04FF\s\.\,\!\?\-\:\(\)«»]/g, '') .replace(/\s+/g, ' ') .replace(/\n+/g, ' ') .trim(); } // Удаляем только очевидный мусор function removeUnwantedElements(element) { const unwantedSelectors = [ 'nav', 'header', 'footer', 'aside', 'menu', '.nav', '.navigation', '.header', '.footer', '.sidebar', '.menu', '.breadcrumb', '.pagination', '.ad', '.advertisement', '.banner', '.ads', '.social', '.share', '.comments', '.comment', 'script', 'style', 'img', 'picture', 'video', 'audio', 'iframe' ]; unwantedSelectors.forEach(selector => { const elements = element.querySelectorAll(selector); elements.forEach(el => el.remove()); }); } // Проверяем валидность контентного элемента function isValidContentElement(element) { if (!element || element.offsetParent === null) return false; const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden') return false; if (element.closest('nav, header, footer, aside, .nav, .header, .footer, .sidebar')) { return false; } return true; } // Функция для разбивки текста на слова function calculateWordBoundaries(text) { const words = text.split(/\s+/); const boundaries = []; let position = 0; for (let word of words) { if (word.trim()) { boundaries.push({ start: position, end: position + word.length, word: word }); position += word.length + 1; } } return boundaries; } // Функция для оценки текущей позиции function estimateCurrentPosition() { if (!speechStartTime) return 0; const elapsedTime = (Date.now() - speechStartTime) / 1000; const estimatedChars = Math.floor(elapsedTime * currentRate * 12); return Math.min(estimatedChars, currentText.length); } // Функция для поиска текущего слова по позиции function findCurrentWordIndex(position) { for (let i = 0; i < wordBoundaries.length; i++) { if (position >= wordBoundaries[i].start && position < wordBoundaries[i].end) { return i; } } return Math.min(position, wordBoundaries.length - 1); } // Функция для начала чтения с позиции function playFromPosition(startPosition = 0) { if (!currentText) { currentText = extractMainText(); if (!currentText || currentText.length < 50) { alert('Не удалось найти текст для чтения.'); return; } wordBoundaries = calculateWordBoundaries(currentText); } const textToRead = currentText.substring(startPosition); if (textToRead.length < 10) return; speechSynthesis.cancel(); speechInstance = new SpeechSynthesisUtterance(textToRead); speechInstance.lang = 'ru-RU'; speechInstance.rate = currentRate; speechStartTime = Date.now(); currentWordIndex = findCurrentWordIndex(startPosition); speechInstance.onend = () => { if (!isManualStop) { speechInstance = null; isPaused = false; currentWordIndex = 0; } isManualStop = false; }; speechInstance.onerror = (event) => { if (!isManualStop && event.error !== 'interrupted') { console.error('Ошибка TTS:', event); } isManualStop = false; }; isManualStop = false; speechSynthesis.speak(speechInstance); } // Основная функция чтения function playText() { if (isPaused && speechInstance) { speechSynthesis.resume(); speechStartTime = Date.now() - (estimatedPosition / (currentRate * 12)) * 1000; isPaused = false; return; } if (speechSynthesis.speaking && !isPaused) { estimatedPosition = estimateCurrentPosition(); playFromPosition(estimatedPosition); return; } currentText = extractMainText(); if (!currentText || currentText.length < 50) { alert('Не удалось найти текст для чтения.'); return; } wordBoundaries = calculateWordBoundaries(currentText); currentWordIndex = 0; playFromPosition(0); } function pauseSpeech() { if (speechSynthesis.speaking && !isPaused) { estimatedPosition = estimateCurrentPosition(); speechSynthesis.pause(); isPaused = true; } } function stopSpeech() { isManualStop = true; speechSynthesis.cancel(); isPaused = false; speechInstance = null; currentWordIndex = 0; } function increaseSpeed() { if (currentRate < 3.0) { currentRate += 0.1; updateSpeedDisplay(); saveSettings(); if (speechSynthesis.speaking && !isPaused) { estimatedPosition = estimateCurrentPosition(); playFromPosition(estimatedPosition); } } } function decreaseSpeed() { if (currentRate > 0.5) { currentRate -= 0.1; updateSpeedDisplay(); saveSettings(); if (speechSynthesis.speaking && !isPaused) { estimatedPosition = estimateCurrentPosition(); playFromPosition(estimatedPosition); } } } function resetSpeedToDefault() { currentRate = 1.0; updateSpeedDisplay(); saveSettings(); } function setupEventListeners() { document.getElementById('ttsPlay').addEventListener('click', playText); document.getElementById('ttsPause').addEventListener('click', pauseSpeech); document.getElementById('ttsStop').addEventListener('click', stopSpeech); document.getElementById('ttsFaster').addEventListener('click', increaseSpeed); document.getElementById('ttsSlower').addEventListener('click', decreaseSpeed); document.getElementById('ttsResetSpeed').addEventListener('click', resetSpeedToDefault); } function makePanelDraggable(panel) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; const header = panel.querySelector('div'); header.onmousedown = dragMouseDown; function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; panel.style.top = (panel.offsetTop - pos2) + "px"; panel.style.left = (panel.offsetLeft - pos1) + "px"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } })();