微信公众号文章朗读助手

采用更稳定的轮询检测机制注入按钮,支持播放、暂停和续播,并使用SVG图标以保证显示效果。

// ==UserScript==
// @name         微信公众号文章朗读助手
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  采用更稳定的轮询检测机制注入按钮,支持播放、暂停和续播,并使用SVG图标以保证显示效果。
// @author       xiangmingya
// @match        https://mp.weixin.qq.com/*
// @grant        GM_addStyle
// @license      MPL-2.0 License
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzQyYjM2YSIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCI+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0zIDl2Nmg0bDUgNVY0TDcgOWg0em0xMy41IDNjMC0xLjc3LTEuMDItMy4yOS0yLjUtNC4wM3Y4LjA1YzEuNDgtLjczIDIuNS0yLjI1IDIuNS00LjAyek0xNCAzLjk4djIuMDZjMi44OS44NiA1IDEuNzEgNSA1Ljk2czEuMjggMy40NSA0IDQuM3YtMi4xYzAtMy44Ny0yLjQzLTYuNjYtNS03Ljl6Ii8+PC9zdmc+
// ==/UserScript==

(function() {
    'use strict';

    console.log('[朗读助手 v2.1] 脚本启动');

    // --- 配置项 ---
    const TARGET_VOICE_NAME = "Microsoft YunJhe Online (Natural) - Chinese (Taiwan)";
    // 使用SVG图标以获得最佳兼容性
    const PLAY_ICON_SVG = `<svg viewBox="0 0 1024 1024" width="14" height="14" style="vertical-align: middle; fill: currentColor;"><path d="M192 128l640 384-640 384z"></path></svg>`;
    const PAUSE_ICON_SVG = `<svg viewBox="0 0 1024 1024" width="14" height="14" style="vertical-align: middle; fill: currentColor;"><path d="M320 128h128v768H320zM576 128h128v768H576z"></path></svg>`;
    const BUTTON_TEXT_PLAY = "朗读";
    const BUTTON_TEXT_PAUSE = "暂停";
    const BUTTON_TEXT_RESUME = "继续";

    let speechState = 'idle'; // 'idle', 'playing', 'paused'
    let speechAPI = window.speechSynthesis;
    let targetVoice = null;
    let speechUtteranceChunks = [];
    let currentChunkIndex = 0;

    /**
     * 使用轮询来等待目标元素出现
     */
    function waitForElement(selector, callback) {
        let interval = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                console.log(`[朗读助手 v2.1] 成功找到目标元素: ${selector}`);
                clearInterval(interval);
                callback(element);
            }
        }, 250); // 每250毫秒检查一次

        // 10秒后如果还没找到,就超时放弃
        setTimeout(() => clearInterval(interval), 10000);
    }

    /**
     * 主初始化函数
     */
    function initializeReader(targetContainer) {
        if (document.getElementById('custom-read-aloud-button')) {
             console.log('[朗读助手 v2.1] 按钮已存在,跳过初始化。');
             return;
        }
        console.log('[朗读助手 v2.1] 开始初始化朗读模块...');

        if (!('speechSynthesis' in window)) {
            console.warn("[朗读助手 v2.1] 浏览器不支持 Web Speech API。");
            return;
        }

        populateVoiceList();
        if (speechAPI.onvoiceschanged !== undefined) {
            speechAPI.onvoiceschanged = populateVoiceList;
        }

        createReadAloudButton(targetContainer);
    }

    function populateVoiceList() {
        const voices = speechAPI.getVoices();
        if (voices.length === 0) return;
        targetVoice = voices.find(voice => voice.name === TARGET_VOICE_NAME) || voices.find(voice => /zh|chinese/i.test(voice.lang));
        if (targetVoice) {
            console.log(`[朗读助手 v2.1] 已选择语音: ${targetVoice.name}`);
        }
    }

    function createReadAloudButton(container) {
        const readButton = document.createElement('span');
        readButton.id = 'custom-read-aloud-button';

        GM_addStyle(`
            #custom-read-aloud-button {
                display: inline-flex; align-items: center; gap: 4px;
                margin-left: 16px; padding: 2px 8px; border-radius: 12px;
                background-color: #f0f0f0; color: #555; cursor: pointer;
                font-size: 14px; transition: all 0.2s ease-in-out; user-select: none;
            }
            #custom-read-aloud-button:hover { background-color: #e0e0e0; transform: scale(1.05); }
            #custom-read-aloud-button.speaking { background-color: #d4edda; color: #155724; }
            #custom-read-aloud-button.paused { background-color: #fff3cd; color: #856404; }
        `);

        readButton.addEventListener('click', mainControl);
        container.insertAdjacentElement('afterend', readButton);
        console.log('[朗读助手 v2.1] 按钮DOM已注入。');
        updateButtonState(speechState); // 在按钮注入后立即更新其内容
    }

    function mainControl() {
        switch (speechState) {
            case 'idle': startReading(); break;
            case 'playing': pauseReading(); break;
            case 'paused': resumeReading(); break;
        }
    }

    function startReading() {
        const contentElement = document.getElementById('js_content');
        if (!contentElement || !contentElement.innerText.trim()) {
            alert("文章内容为空或无法找到。");
            return;
        }

        speechAPI.cancel();
        const textToRead = contentElement.innerText;
        const chunks = textToRead.match(/[^.!?\n,。!?\s]+[.!?\n,。!?\s]?/g) || [text];
        speechUtteranceChunks = chunks.map(chunk => {
            const utterance = new SpeechSynthesisUtterance(chunk.trim());
            utterance.voice = targetVoice;
            utterance.lang = targetVoice ? targetVoice.lang : 'zh-CN';
            utterance.rate = 1.0;
            return utterance;
        });

        currentChunkIndex = 0;
        speechState = 'playing';
        updateButtonState(speechState);
        playNextChunk();
    }

    function pauseReading() {
        speechAPI.pause();
        speechState = 'paused';
        updateButtonState(speechState);
    }

    function resumeReading() {
        speechAPI.resume();
        speechState = 'playing';
        updateButtonState(speechState);
    }

    function resetReading() {
        speechAPI.cancel();
        speechState = 'idle';
        updateButtonState(speechState);
    }

    function playNextChunk() {
        if (currentChunkIndex >= speechUtteranceChunks.length) {
            resetReading();
            return;
        }
        const utterance = speechUtteranceChunks[currentChunkIndex];
        utterance.onend = () => {
            if (speechState === 'playing') {
                currentChunkIndex++;
                playNextChunk();
            }
        };
        utterance.onerror = (event) => {
            console.error('[朗读助手 v2.1] 朗读错误:', event.error);
            resetReading();
        };
        speechAPI.speak(utterance);
    }

    function updateButtonState(state) {
        const button = document.getElementById('custom-read-aloud-button');
        if (!button) {
            console.warn('[朗读助手 v2.1] 更新状态时未找到按钮。');
            return;
        }

        console.log(`[朗读助手 v2.1] 更新按钮状态为: ${state}`);
        button.classList.remove('speaking', 'paused');

        switch (state) {
            case 'playing':
                button.innerHTML = `${PAUSE_ICON_SVG} <span class="button-text">${BUTTON_TEXT_PAUSE}</span>`;
                button.classList.add('speaking');
                break;
            case 'paused':
                button.innerHTML = `${PLAY_ICON_SVG} <span class="button-text">${BUTTON_TEXT_RESUME}</span>`;
                button.classList.add('paused');
                break;
            case 'idle':
            default:
                button.innerHTML = `${PLAY_ICON_SVG} <span class="button-text">${BUTTON_TEXT_PLAY}</span>`;
                break;
        }
    }

    // --- 脚本执行入口 ---
    // 等待 #meta_content_hide_info 元素出现后,再执行初始化
    waitForElement('#meta_content_hide_info', initializeReader);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址