Jump to content

User:Phlsph7/WikiNarrator.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
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 = '';
    const isFirefox = (navigator.userAgent.toLowerCase().indexOf('firefox') > -1);

    speechSynthesis.onvoiceschanged = initialize;
    speechSynthesis.getVoices(); // adjustment to make it work for firefox

    $.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();
            if(!('speechSynthesis' in window)){
                mw.notify("WikiNarrator does not work in your browser because it does not support speech synthesis.");
            }
            else if(speechSynthesis.getVoices().length == 0){
                mw.notify("Unable to open WikiNarrator because voices are still loading. Please try later. If you keep getting this error message, it could mean that no voices are installed on your system.");
            }
            else{
                initialize(); // just in case the initialize function has not been called yet for slow PCs
                let settings = getSettingsFromStorage();
                if(settings.showPlayer){
                    settings.showPlayer = false;
                    saveSettingsToStorage(settings);
                    stop();
                    hidePlayer();
                }
                else{
                    settings.showPlayer = true;
                    saveSettingsToStorage(settings);
                    showPlayer();
                }
            }
        };
    });

    function initialize(){
        if(!initialized){
            initialized = true;
            if(getSettingsFromStorage() === null){
                let settings = getDefaultSettings();
                saveSettingsToStorage(settings);
            }
            initializePlayer();

            if(getSettingsFromStorage().showPlayer){
                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 = [''];

            if(isFirefox){
                textChunks = [text];
            }
            else{
                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();
                if(settings.voiceName != ''){
                    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(){
            if(!isFirefox){
                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();
        
        // sort voices by language and name
        voices.sort((a, b) => a.name.localeCompare(b.name));
		voices.sort((a, b) => a.lang.localeCompare(b.lang));
		
        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 == '' && filteredVoices.length > 0){
            defaultVoiceName = filteredVoices[0].name;
        }
        return {
            voiceName: defaultVoiceName,
            volume: 1,
            speed: 1,
            pitch: 1,
            showPlayer: true,
        };
    }
})();