KaniWani audio

Play audio in KaniWani upon correct answers

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         KaniWani audio
// @namespace    http://tampermonkey.net/
// @version      0.27
// @description  Play audio in KaniWani upon correct answers
// @author       voithos
// @match        *://*.kaniwani.com/*
// @grant        GM_xmlhttpRequest
// @license MIT
// ==/UserScript==


// Based on original version by CometZero:
// https://greasyfork.org/en/scripts/25373-kaniwani-audio

var buttonHtml = `<button id="playAudio">Play Audio</button>`;
var colorDisabled = "hsl(0, 0%, 65%)";

var loadingImageHtml = `<img src="http://img.etimg.com/photo/45627788.cms"
alt="Loading ..." style="margin-left:5px;width:15px;height:15px;display:none;">`;

var playSoundButton;
var loadingImage;

var audioWord = null;
var audio = null;
var lastAudio = null;
var isPendingPlay = false;
var isLoadingAudio = false;


var onAudioReady = function(audioNode){
    audio = audioNode;
    audio.style.display = 'none';
    document.body.appendChild(audio);

    isLoading = false;
    loadingImage.style.display = "none"; // hide loading image
    playSoundButton.style.color = ""; // set default text color

    if(isPendingPlay){
        audio.play();
        isPendingPlay = false;
    }
};

var onAudioLoading = function(){
    loadingImage.style.display = "inline"; // show loading image
    playSoundButton.style.color = colorDisabled; // dimm play button
    isLoading = true;
};


(function() {
    'use strict';

    initWhenReady();
})();

// wait until the first word has been loaded so that we can complete the setup
function initWhenReady(){
    console.log("Trying to initialize KaniWani audio...");
    var wordDom = getWordDom();
    if(wordDom){
        console.log("KaniWani audio initialized");
        init();
    }else{
        //wait a bit and then try again, assuming the initial loading will be complete soon
        setTimeout(function(){ initWhenReady(); }, 500);
    }
}

function init(){
    initElements();

    // loads audio for the first time;
    loadAudio();

    onNewWordObserver(function(mutations, observer) {
        // loads audio everytime the word DOM changes
        loadAudio();
    });
  
    onCorrectAnswerObserver(function() {
        playAudio();
    });

    playSoundButton.onclick = function(){
        playAudio();
    };
  
    var initialLocation = location.pathname;
    // check to make sure that the location hasn't changed
    // if it has, redo the initialization
    var intervalId = setInterval(function(){
        if (location.pathname !== initialLocation) {
            clearInterval(intervalId);
            initWhenReady();
        }
    }, 1000);
}

// plays the audio
// finds the word than loads the audo if needed and plays it
function playAudio(){
    var word = getWord();

    if(!word) {
        console.log("Cannot get word for audio :(");
        return;
    }

    // if audio is available just play it
    if(audio){
        audio.play();
        return;
    }
    // audio is not available we need to load it

    // make sure audio is not already loading
    if(!isLoadingAudio){
        loadAudio(word);
    }

    // set pendingPlay true so when it loads it will play the audio
    isPendingPlay = true;
}

// accepts function that is triggered when new word is shown
function onNewWordObserver(f){
    MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

    var observer = new MutationObserver(f);

    // configuration of the observer:
    var config = { attributes: true, childList: true, characterData: true };

    // select the target node
    var target = getWordDom();

    // pass in the target node, as well as the observer options
    observer.observe(target.parentNode.parentNode, config);
}

// accepts function that is triggered when user has answered correctly
function onCorrectAnswerObserver(f){
    MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

    var observer = new MutationObserver(function() {
        var target = document.querySelector('form > div');
console.log(window.getComputedStyle(target).backgroundColor);
        if (target && ( window.getComputedStyle(target).backgroundColor === 'rgb(127, 212, 104)' || window.getComputedStyle(target).backgroundColor === 'rgb(68, 119, 51)')) {
            // Answer was correct!
            f();
        }
    });

    // configuration of the observer:
    var config = { attributes: true, childList: true, characterData: true };

    var target = document.querySelector('form');
    // pass in the target node, as well as the observer options
    observer.observe(target, config);
}

// adds all the buttons and loading images to the webpage
function initElements(){
    // create "play audio button"
    playSoundButton = htmlToElement(buttonHtml);
    loadingImage = htmlToElement(loadingImageHtml);
    playSoundButton.appendChild(loadingImage);

    // insert
    var addSynonymButton = document.evaluate("//button[contains(., 'Add Synonym')]", document, null, XPathResult.ANY_TYPE, null ).iterateNext();
    if (addSynonymButton) {
        playSoundButton.className = addSynonymButton.className;
        addSynonymButton.parentNode.appendChild(playSoundButton);
    }
}

// get the dom that is containg word that it has to play
function getWordDom(){
    var answers = document.querySelectorAll('[data-answer=true]');
    if (answers && answers.length >= 1) {
        var first = answers[0];
        return first.querySelector('[data-ruby]');
    }
    return null;
}

// finds the word that it has to play
function getWord(){
    var kanjisDom = getWordDom();

    if(!kanjisDom){
        return null;
    }

    // get the word
    var word = kanjisDom.dataset.ruby;
    if (word) {
        var parts = word.split(/\s+/);
        if (parts.length !== 2) {
            console.log(`Malformed ruby answer! '${word}'`);
            return null;
        }
        return parts[0];
    }
}

// get audio url for a word and play it
function loadAudio(){
    var vocabKanji = getWord();
    if(!vocabKanji) throw "vocabKanji cannot be empty!";

    if (vocabKanji === audioWord) return;
    audioWord = vocabKanji;

    // keep the last audio element around in case it's currently being played,
    // or else the audio might get cut off.
    if (lastAudio) {
        document.body.removeChild(lastAudio);
    }
    if (audio) {
        lastAudio = audio;
    }
    audio = null;
    onAudioLoading();

    GM_xmlhttpRequest ( {
        method: 'GET',
        url:    `https://www.wanikani.com/vocabulary/${vocabKanji}`,
        accept: 'text/xml',
        onreadystatechange: function (response) {
            if (response.readyState != 4)
                return;

            // get responseTxt
            var responseTxt = response.responseText;
            var domRoot = document.createElement('html');
            domRoot.innerHTML = responseTxt;
            var audioNode = domRoot.querySelector('audio');
            if (audioNode) {
                onAudioReady(audioNode);
            }
        }
    } );
}

/**
 * Creates dom element from string.
 * @param {String} HTML representing a single element
 * @return {Element}
 */
function htmlToElement(html) {
    var template = document.createElement('template');
    template.innerHTML = html;
    return template.content.firstChild;
}