您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Простой скрипт для чтения вслух основного текста страницы с перемещаемой панелью управления.
当前为
// ==UserScript== // @name Простой Ридер Текста // @namespace http://tampermonkey.net/ // @version 1.7 // @description Простой скрипт для чтения вслух основного текста страницы с перемещаемой панелью управления. // @author You // @match *://*/* // @grant none // @run-at document-end // @license MIT // ==/UserScript== /* * MIT License * * Copyright (c) 2024 Простой Ридер Текста * * Данная лицензия разрешает лицам, получившим копию данного программного обеспечения * и сопутствующей документации (в дальнейшем «Программное обеспечение»), безвозмездно * использовать Программное обеспечение без ограничений, включая неограниченное право * на использование, копирование, изменение, объединение, публикацию, распространение, * сублицензирование и/или продажу копий Программного обеспечения, а также лицам, которым * предоставляется данное Программное обеспечение, при соблюдении следующих условий: * * Указанное выше уведомление об авторском праве и данные условия должны быть включены во * все копии или значимые части Программного обеспечения. * * ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, * ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОЙ * ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И НЕНАРУШЕНИЯ ПРАВ. НИ В КАКОМ * СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, * УБЫТКОВ ИЛИ ДРУГИХ ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩЕМУ ПРАВУ, ИЛИ ПО ИНЫМ ПРИЧИНАМ, ВОЗНИКШИМ * ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ. */ (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> `; // Стили для панели 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: '120px', fontFamily: 'Arial, sans-serif' }); document.body.appendChild(controlPanel); return controlPanel; } // Функции для работы с localStorage function saveSpeedToStorage(speed) { try { localStorage.setItem('ttsReadingSpeed', speed.toString()); } catch (e) { console.log('Не удалось сохранить скорость в localStorage:', e); } } function loadSpeedFromStorage() { try { const savedSpeed = localStorage.getItem('ttsReadingSpeed'); if (savedSpeed) { const speed = parseFloat(savedSpeed); // Проверяем, что значение в допустимом диапазоне if (speed >= 0.5 && speed <= 3.0) { return speed; } } } catch (e) { console.log('Не удалось загрузить скорость из localStorage:', e); } return 1.0; // Значение по умолчанию } function resetSpeedToDefault() { currentRate = 1.0; updateSpeedDisplay(); saveSpeedToStorage(currentRate); } // Ждем полной загрузки DOM if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { // Даем небольшую задержку для полной инициализации страницы setTimeout(() => { const controlPanel = createControlPanel(); const supportPanel = createSupportPanel(); // Создаем панель поддержки if (controlPanel) { // Загружаем сохраненную скорость ДО настройки обработчиков currentRate = loadSpeedFromStorage(); setupEventListeners(); makePanelDraggable(controlPanel); updateSpeedDisplay(); } }, 1000); } // Переменные для управления речью 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 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 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; // +1 для пробела } } return boundaries; } // Функция для оценки текущей позиции в тексте function estimateCurrentPosition() { if (!speechStartTime) return 0; const elapsedTime = (Date.now() - speechStartTime) / 1000; // в секундах const estimatedChars = Math.floor(elapsedTime * currentRate * 15); // ~15 символов в секунду при скорости 1.0 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 extractMainText() { // Специфичные селекторы для новостных сайтов const selectors = [ 'article', 'main', '[role="main"]', '.content', '.post-content', '.article', '.news-text', '.text', '.story', '[class*="content"]', '[class*="article"]', '[class*="text"]', // Специфичные для interfax '.at', '.textMT', '.article-content' ]; for (let selector of selectors) { const elements = document.querySelectorAll(selector); for (let element of elements) { const text = element.textContent.trim(); if (text.length > 200 && !isNavigation(element)) { return cleanText(text); } } } // Альтернативный подход: ищем самый большой текстовый блок const allElements = document.querySelectorAll('p, div, section, article'); let bestElement = null; let maxLength = 0; for (let element of allElements) { const text = element.textContent.trim(); if (text.length > maxLength && !isNavigation(element)) { maxLength = text.length; bestElement = element; } } if (bestElement && maxLength > 200) { return cleanText(bestElement.textContent); } // Если ничего не нашли, берем body return cleanText(document.body.textContent); } // Функция для проверки, является ли элемент навигацией function isNavigation(element) { const navSelectors = ['nav', 'header', 'footer', 'menu', '.nav', '.header', '.footer', '.menu']; const tagName = element.tagName.toLowerCase(); const className = element.className.toLowerCase(); return navSelectors.some(selector => tagName === selector.replace('.', '') || className.includes(selector.replace('.', '')) ); } // Функция для очистки текста function cleanText(text) { return text .replace(/\s+/g, ' ') .replace(/\n+/g, ' ') .trim(); } // Функция для начала чтения с определенной позиции 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) { // Достигнут конец текста speechInstance = null; isPaused = false; currentWordIndex = 0; 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) => { console.error('Ошибка синтеза речи:', event); if (!isManualStop && event.error !== 'interrupted') { alert('Произошла ошибка при синтезе речи. Проверьте поддержку TTS в вашем браузере.'); } isManualStop = false; }; isManualStop = false; speechSynthesis.speak(speechInstance); } // Функция для начала или возобновления чтения function playText() { if (isPaused && speechInstance) { speechSynthesis.resume(); speechStartTime = Date.now() - (estimatedPosition / (currentRate * 15)) * 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 setupEventListeners() { const playBtn = document.getElementById('ttsPlay'); const pauseBtn = document.getElementById('ttsPause'); const stopBtn = document.getElementById('ttsStop'); const fasterBtn = document.getElementById('ttsFaster'); const slowerBtn = document.getElementById('ttsSlower'); const resetBtn = document.getElementById('ttsResetSpeed'); if (playBtn) playBtn.addEventListener('click', playText); if (pauseBtn) pauseBtn.addEventListener('click', pauseSpeech); if (stopBtn) stopBtn.addEventListener('click', stopSpeech); if (fasterBtn) fasterBtn.addEventListener('click', increaseSpeed); if (slowerBtn) slowerBtn.addEventListener('click', decreaseSpeed); if (resetBtn) resetBtn.addEventListener('click', resetSpeedToDefault); } 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(); saveSpeedToStorage(currentRate); // Перезапускаем с текущей позиции if (speechSynthesis.speaking && !isPaused) { estimatedPosition = estimateCurrentPosition(); playFromPosition(estimatedPosition); } } } function decreaseSpeed() { if (currentRate > 0.5) { currentRate -= 0.1; updateSpeedDisplay(); saveSpeedToStorage(currentRate); // Перезапускаем с текущей позиции if (speechSynthesis.speaking && !isPaused) { estimatedPosition = estimateCurrentPosition(); playFromPosition(estimatedPosition); } } } // Функция для реализации перетаскивания панели function makePanelDraggable(panel) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; const header = panel.querySelector('div'); if (!header) return; header.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; 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; } } })();