Jump to content

User:Phlsph7/WikiNarrator.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
(function(){
    const scriptName = 'WikiNarrator';
    const intervalDelay = 5000;
    let intervalId = '';
    const states = {stopped: 0, playing: 1, paused: 2};
    let currentState;
    let initialized = false;
    let selectedText = '';
    
    speechSynthesis.onvoiceschanged = function(){
        if(!initialized){
            initialized = true;
            if(getSettingsFromStorage() === null){
                let settings = getDefaultSettings();
                saveSettingsToStorage(settings);
            }
            initializePlayer();

            if(getSettingsFromStorage().showPlayer){
                showPlayer();
            }
        }
    };

    $.when(mw.loader.using('mediawiki.util'), $.ready).then(function(){
        const portletLink = mw.util.addPortletLink('p-tb', '#', 'Show/hide ' + scriptName, scriptName + 'Id');
        portletLink.onclick = function(e) {
            e.preventDefault();
            let settings = getSettingsFromStorage();
            if(settings.showPlayer){
                settings.showPlayer = false;
                saveSettingsToStorage(settings);
                stop();
                hidePlayer();
            }
            else{
                settings.showPlayer = true;
                saveSettingsToStorage(settings);
                showPlayer();
            }
        };
    });

    function initializePlayer(){
        const htmlCode = `<div id="wikiNarratorContainer" style="position: fixed; right: 10px; bottom: 10px; display: none;">
    <style>
        .wiki-narrator-container{
            background-color: #eee;
            padding: 5px;
            border-radius: 10px;
            border: 1px solid #ccc;
        }
        .wiki-narrator-control-button {
            font-size: 1.5em;
            flex: 1;
            margin: 2px;
            padding-bottom: 4px;
            width: 40px;
            height: 40px;
        }
        .wiki-narrator-settings-button {
            flex: 1;
            margin: 2px;
        }
        .wiki-narrator-item {
            margin-bottom: 15px;
        }
        .wiki-narrator-label{
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        .wiki-narrator-control{
            width: 100%;
        }
    </style>
    <div class="wiki-narrator-container" style="width: 300px; margin-bottom: 5px; display: none;" id="wikiNarratorSettingsContainer">
        <div style="margin-bottom: 10px; text-align: center; font-size: 1.5em;">Settings</div>
        <div class="wiki-narrator-item">
            <label for="voice" class="wiki-narrator-label">Voice</label>
            <select id="wikiNarratorVoice" class="wiki-narrator-control"></select>
        </div>
        <div class="wiki-narrator-item">
            <label for="volume" class="wiki-narrator-label">Volume</label>
            <input id="wikiNarratorVolume" type="range" min="0" max="1" step="0.1" value="1" class="wiki-narrator-control">
        </div>
        <div class="wiki-narrator-item">
            <label for="speed" class="wiki-narrator-label">Speed</label>
            <input id="wikiNarratorSpeed" type="range" min="0.1" max="2" step="0.1" value="1" class="wiki-narrator-control">
        </div>
        <div class="wiki-narrator-item">
            <label for="pitch" class="wiki-narrator-label">Pitch</label>
            <input id="wikiNarratorPitch" type="range" min="0" max="2" step="0.1" value="1" class="wiki-narrator-control">
        </div>
        <div style="display: flex">
            <button id="wikiNarratorDefault" class="wiki-narrator-settings-button">Default</button>
            <button id="wikiNarratorClose"class="wiki-narrator-settings-button">Close</button>
        </div>
	</div>
    <div style="display:flex">
        <div style="flex:1"></div>
        <div id="wikiNarratorControlButtons" class="wiki-narrator-container" style="display: flex;">
            <button id="wikiNarratorPlay" class="wiki-narrator-control-button" title="Play">&#9654;</button>
            <button id="wikiNarratorPause" class="wiki-narrator-control-button" title="Pause">&#9208;</button>
            <button id="wikiNarratorStop" class="wiki-narrator-control-button" title="Stop">&#9632;</button>
            <button id="wikiNarratorSettings" class="wiki-narrator-control-button" style="font-weight: bold;"  title="Settings">&#9881;</button>
        </div>
    </div>
</div>`;
        const container = document.createElement('div');
        document.body.append(container);
        container.outerHTML = htmlCode;
        if(!('speechSynthesis' in window)){
            wikiNarratorControlButtons.innerHTML = '<p><b>WikiNarrator</b> does not work in your browser<br>because it does <b>not support</b> <a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis">speech synthesis</a>.</p>';
        }
        else{
            adjustState(states.stopped);
            wikiNarratorVoice.onchange = saveSettingsFromInput;
            wikiNarratorVolume.onchange = saveSettingsFromInput;
            wikiNarratorSpeed.onchange = saveSettingsFromInput;
            wikiNarratorPitch.onchange = saveSettingsFromInput;
            wikiNarratorSettings.onclick = showHideSettings;
            wikiNarratorPlay.onmousedown = function(){
                hideRefs();
                selectedText = getSelection().toString();
                showRefs();
            };
            wikiNarratorPlay.onclick = play;
            wikiNarratorPause.onclick = pause;
            wikiNarratorStop.onclick = stop;
            wikiNarratorDefault.onclick = setDefaultSettings;
            wikiNarratorClose.onclick = showHideSettings;
            initializeInput();
        }
    }

    function showPlayer(){
        wikiNarratorContainer.style.display = '';
    }

    function hidePlayer(){
        wikiNarratorContainer.style.display = "none";
    }

    function play(){
        if(currentState == states.paused){
            adjustState(states.playing);
            speechSynthesis.resume();
            setSpeechInterval();
        }
        else{
            adjustState(states.playing);
            let settings = getSettingsFromStorage();
            let text = selectedText;
            if(text == ''){
                hideRefs();
                let articleContainer;
                if(document.getElementById('wikiPreview')){
                    articleContainer = document.getElementById('wikiPreview').querySelector('.mw-parser-output');
                }
                else{
                    articleContainer = document.getElementById('mw-content-text').querySelector('.mw-parser-output');
                }
                let readableElements = Array.from(articleContainer.querySelectorAll(':scope > p, :scope > blockquote, :scope > ul, :scope > ol, :scope > dl, :scope > .mw-heading'));
                for(let element of readableElements){
                    text += element.innerText + '\n';
                }
                showRefs();
            }
            
            text = text.split('\r').join('\n')
                .split('\n\n').join('\n')
                .split('\n.').join('\n');
            let textChunks = [''];
            const chunkSize = 2000;
            let sentences = text.split('. ');
            let currentIndex = 0;
            for(let sentence of sentences){
            	if(sentence.trim().length > 2){
	                if(textChunks[currentIndex].length + sentence.length < chunkSize){
	                    textChunks[currentIndex] += sentence + '. ';
	                }
	                else{
	                    currentIndex++;
	                    textChunks[currentIndex] = sentence + '. ';
	                }
            	}
            }

            for(let i = 0; i < textChunks.length; i++){
                const target = "\n. ";
                if (textChunks[i].endsWith(target)) {
                    textChunks[i] = textChunks[i].slice(0, -target.length);
                }
                textChunks[i] = textChunks[i].trim();
            }

            let utterances = [];
            for(let i = 0; i < textChunks.length; i++){
                let utterance = new SpeechSynthesisUtterance();
                utterance.text = textChunks[i].trim();
                utterance.voice = speechSynthesis.getVoices().filter(function(voice) { return voice.name == settings.voiceName; })[0];
                utterance.volume = settings.volume;
                utterance.rate = settings.speed;
                utterance.pitch = settings.pitch;
                utterances.push(utterance);
            }

            for(let i = 0; i < utterances.length - 1; i++){
                let currentUtterance = utterances[i];
                let nextUtterance = utterances[i+1];
                currentUtterance.onend = function(){
                    if(currentState == states.playing){
                        clearInterval(intervalId);
                        speechSynthesis.cancel();
                        speechSynthesis.speak(nextUtterance);
                        setSpeechInterval();
                    }
                    else{
                        adjustState(states.stopped);
                        clearInterval(intervalId);
                    }
                };
            }
            
            utterances[utterances.length-1].onend = function() {
                adjustState(states.stopped);
                clearInterval(intervalId);
            };

            speechSynthesis.cancel();
            speechSynthesis.speak(utterances[0]);
            setSpeechInterval();
        }
    }
    
    function pause(){
        adjustState(states.paused);
        clearInterval(intervalId);
        speechSynthesis.pause();
    }

    function stop(){
        adjustState(states.stopped);
        clearInterval(intervalId);
        speechSynthesis.cancel();
    }

    function adjustState(newState){
        currentState = newState;
        switch(currentState){
            case states.stopped:
                wikiNarratorPlay.disabled = false;
                wikiNarratorPause.disabled = true;
                wikiNarratorStop.disabled = true;
                break;
            case states.playing:
                wikiNarratorPlay.disabled = true;
                wikiNarratorPause.disabled = false;
                wikiNarratorStop.disabled = false;
                break;
            case states.paused:
                wikiNarratorPlay.disabled = false;
                wikiNarratorPause.disabled = true;
                wikiNarratorStop.disabled = false;
                break;
        }
    }

    function setSpeechInterval(){
        intervalId = setInterval(function(){
            speechSynthesis.pause();
            speechSynthesis.resume();
        }, intervalDelay);
    }

    function showHideSettings(){
        if(wikiNarratorSettingsContainer.style.display != "none"){
            wikiNarratorSettingsContainer.style.display = "none";
        }
        else{
            wikiNarratorSettingsContainer.style.display = '';
        }
    }

    function hideRefs(){
		let refs = document.body.querySelectorAll('.reference, .Inline-Template, .mw-editsection');
		for(let ref of refs){
			ref.style.display = 'none';
		}
	}

	function showRefs(){
		let refs = document.body.querySelectorAll('.reference, .Inline-Template, .mw-editsection');
		for(let ref of refs){
			ref.style.display = '';
		}
	}

    function getSettingsFromStorage(){
        return JSON.parse(localStorage.getItem('wikiNarratorSettings'));
    }

    function getSettingsFromInput(){
        let settings = {
            voiceName: wikiNarratorVoice.value,
            volume: wikiNarratorVolume.value,
            speed: wikiNarratorSpeed.value,
            pitch: wikiNarratorPitch.value,
            showPlayer: true,
        };
        return settings;
    }

    function saveSettingsToStorage(settings){
        localStorage.setItem('wikiNarratorSettings', JSON.stringify(settings));
    }

    function initializeInput(){
        let settings = getSettingsFromStorage();
        wikiNarratorVolume.value = settings.volume;
        wikiNarratorSpeed.value = settings.speed;
        wikiNarratorPitch.value = settings.pitch;

        wikiNarratorVoice.innerHTML = '';
        let voices = speechSynthesis.getVoices();
        for(let voice of voices){
            const option = document.createElement('option');
            option.value = voice.name;
            option.text = voice.name;
            if(voice.name == settings.voiceName){
                option.selected = true;
            }
            wikiNarratorVoice.add(option);
        }
    }

    function saveSettingsFromInput(){
        let settings = getSettingsFromInput();
        saveSettingsToStorage(settings);
    }

    function setDefaultSettings(){
        let settings = getDefaultSettings();
        saveSettingsToStorage(settings);
        initializeInput();
    }

    function getDefaultSettings(){
        let voices = speechSynthesis.getVoices();
        let filteredVoices = [];
        for(let voice of voices){
            if(voice.lang.includes('en-')){
                filteredVoices.push(voice);
            }
        }
        if(filteredVoices.length == 0){
            filteredVoices = voices;
        }

        let defaultVoiceName = '';
        for(let voice of filteredVoices){
            if(voice.default == true){
                defaultVoiceName = voice.name;
                break;
            }
        }
        if(defaultVoiceName == ''){
            defaultVoiceName = filteredVoices[0].name;
        }
        return {
            voiceName: defaultVoiceName,
            volume: 1,
            speed: 1,
            pitch: 1,
            showPlayer: true,
        };
    }
})();