Advanced Context Sentence

Link the kanji page for the kanji in the context sentence section

Ekde 2019/02/11. Vidu La ĝisdata versio.

"use strict";

// ==UserScript==
// @name         Advanced Context Sentence
// @namespace    https://openuserjs.org/users/abdullahalt
// @version      1.20
// @description  Link the kanji page for the kanji in the context sentence section
// @author       abdullahalt
// @match        https://www.wanikani.com/lesson/session
// @match        https://www.wanikani.com/review/session
// @match        https://www.wanikani.com/vocabulary/*
// @grant        none
// @copyright    2019, abdullahalt (https://openuserjs.org//users/abdullahalt)
// @license MIT
// ==/UserScript==
// ==OpenUserJS==
// @author abdullahalt
// ==/OpenUserJS==

(function() {
  //-----------f------------------------------------------------------------------------------------------------------------------------------------------//
  //-------------------------------------------------------------------INITIALIZATION--------------------------------------------------------------------//
  //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  var wkof = window.wkof;
  var scriptId = "AdvancedContextSentence";
  var scriptName = "Advanced Context Sentence";
  var vocabularyPage = "/vocabulary";
  var sessions = [
    {
      page: "/review/session",
      mount: "#item-info-col2",
      loading: "#loading",
      getHeader: function getHeader(sentences) {
        return sentences[0].previousElementSibling;
      }
    },
    {
      page: "/lesson/session",
      mount: "#supplement-voc-context-sentence",
      loading: "#loading-screen",
      getHeader: function getHeader(sentences) {
        return sentences[0].parentElement.previousElementSibling;
      }
    }
  ];
  var state = {
    guruedKanjiColor: "#f100a1",
    unguruedKanjiColor: "#888888"
  }; // Application start Point

  main();

  function main() {
    // we don't need to observe any changes in the vocabulary page
    if (isPage(vocabularyPage)) {
      init(function(guruedKanji) {
        return evolveContextSentence(guruedKanji, function(sentences) {
          return sentences[0].previousElementSibling;
        });
      });
      return;
    } // Get the target for the session page to watch for changes

    var session = getSessionDependingOnPage();
    if (session) startObserving(session);
  }

  function startObserving(_ref) {
    var mount = _ref.mount,
      loading = _ref.loading,
      getHeader = _ref.getHeader;
    var loadingObservationConfiguration = {
      attributes: true,
      childList: false,
      subtree: false
    };
    var itemInfoObservationConfiguration = {
      attributes: false,
      childList: true,
      subtree: false
    };

    var observeLoading = function observeLoading() {
      observeChanges({
        element: loading,
        config: loadingObservationConfiguration,
        onChange: runInit
      });
    };

    var runInit = function runInit() {
      init(function(guruedKanji) {
        observeSentenceChanges(guruedKanji);
      });
    };

    var observeSentenceChanges = function observeSentenceChanges(guruedKanji) {
      observeChanges({
        element: mount,
        continuesObservation: true,
        config: itemInfoObservationConfiguration,
        onChange: function onChange() {
          return evolve(guruedKanji);
        },
        onInitObserver: function onInitObserver() {
          return evolve(guruedKanji);
        }
      });
    };

    var evolve = function evolve(guruedKanji) {
      return evolveContextSentence(guruedKanji, getHeader);
    };
    /**
     * Basically, this function will fire an observer that will
     * watch when the loading screen on the session pages (lesson and review) stops,
     * then it will fire another observer to watch for changing the sentences,
     * whenever the sentence change it will fire the evolveContextSentence over it again
     *
     * why wait for the loading screen stops? because the script slows down the animation
     * which makes a really bad user experience
     */

    observeLoading();
  }

  function init(callback) {
    console.log("init acs");

    if (wkof) {
      wkof.include("ItemData,Settings");
      wkof
        .ready("ItemData,Settings")
        .then(loadSettings)
        .then(proccessLoadedSettings)
        .then(getGuruedKanji)
        .then(extractKanjiFromResponse)
        .then(callback);
    } else {
      console.warn(
        scriptName +
          ": You are not using Wanikani Open Framework which " +
          "this script utlizes to see the kanji you learned and highlights it with a different color, " +
          "it also provides the settings dailog for the scrip. " +
          "You can still use Advanced Context Sentence normally though"
      );
      callback();
    }
  }

  function evolveContextSentence() {
    var guruedKanji =
      arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
    var getHeader = arguments.length > 1 ? arguments[1] : undefined;
    createReferrer();
    var sentences = document.querySelectorAll(".context-sentence-group");
    if (sentences.length === 0) return;
    if (wkof) evolveHeader(getHeader(sentences));
    sentences.forEach(function(sentence) {
      console.log(sentence);
      var japaneseSentence = sentence.querySelector('p[lang="ja"]');
      var audioButton = createAudioButton(japaneseSentence.innerHTML);
      var advancedExampleSentence = "";
      var chars = japaneseSentence.innerHTML.split("");
      chars.forEach(function(char) {
        var renderedChar = tagAndLinkKanji(char, guruedKanji);
        advancedExampleSentence = advancedExampleSentence.concat(renderedChar);
      });
      japaneseSentence.innerHTML = advancedExampleSentence;
      highlightKanji();
      japaneseSentence.append(audioButton);
    });
  }

  function evolveHeader(header) {
    var settings = document.createElement("i");
    settings.setAttribute("class", "icon-gear");
    settings.setAttribute(
      "style",
      "font-size: 14px; cursor: pointer; vertical-align: middle; margin-left: 10px;"
    );
    settings.onclick = openSettings;
    header.append(settings);
  }

  function createAudioButton(sentence) {
    var mpegSource = createSource("audio/mpeg", sentence);
    var oogSource = createSource("audio/oog", sentence);
    var audio = document.createElement("audio");
    audio.setAttribute("display", "none");
    audio.append(mpegSource, oogSource);
    var button = document.createElement("button");
    button.setAttribute("class", "audio-btn audio-idle"); // Handle events

    button.onclick = function() {
      return audio.play();
    };

    audio.onplay = function() {
      return button.setAttribute("class", "audio-btn audio-play");
    };

    audio.onended = function() {
      return button.setAttribute("class", "audio-btn audio-idle");
    }; // return audio and button as sibiling elements

    var audioContainer = document.createElement("span");
    audioContainer.append(button, audio);
    return audioContainer;
  }

  function observeChanges(params) {
    var element = params.element,
      config = params.config,
      onChange = params.onChange,
      _params$onInitObserve = params.onInitObserver,
      onInitObserver =
        _params$onInitObserve === void 0
          ? function() {}
          : _params$onInitObserve,
      _params$continuesObse = params.continuesObservation,
      continuesObservation =
        _params$continuesObse === void 0 ? false : _params$continuesObse;

    if (!window.MutationObserver) {
      console.warn(
        scriptName +
          ": you're browser does not support MutationObserver " +
          "which this script utilaizes to implement its features in /lesson/session and /review/sesson. " +
          "update you're broswer or use another one if you want Advanced Context Sentence to work on them." +
          "This script is still useful on /vocabulary page though"
      );
      return;
    }

    onInitObserver();
    var target = document.querySelector(element);
    var observer = new MutationObserver(function() {
      observer.disconnect();
      onChange();
      continuesObservation && observer.observe(target, config);
    });
    observer.observe(target, config);
  } //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  //-------------------------------------------------------------------SETTINGS--------------------------------------------------------------------------//
  //-----------------------------------------------------------------------------------------------------------------------------------------------------//

  function loadSettings() {
    return wkof.Settings.load(scriptId, state);
  }

  function proccessLoadedSettings() {
    state = wkof.settings[scriptId];
    console.log(state);
  }

  function openSettings() {
    var config = {
      script_id: scriptId,
      title: scriptName,
      on_save: updateSettings,
      content: {
        highlightColors: {
          type: "section",
          label: "Highlight Colors" // A string that will appear in the section.
        },
        guruedKanjiColor: {
          type: "color",
          label: "Gurued Kanji",
          hover_tip:
            "Kanji you have on Guru or higher will be highlited using this color",
          default: state.guruedKanjiColor
        },
        unguruedKanjiColor: {
          type: "color",
          label: "Ungurued Kanji",
          hover_tip:
            "Kanji you have on Apprentice or have never been unlucked will be highlited using this color",
          default: state.unguruedKanjiColor
        }
      }
    };
    var dialog = new wkof.Settings(config);
    dialog.open();
  } // Called when the user clicks the Save button on the Settings dialog.

  function updateSettings() {
    state = wkof.settings[scriptId];
    highlightKanji();
  } //-----------------------------------------------------------------------------------------------------------------------------------------------------//
  //-------------------------------------------------------------------HELPER FUNCTIONS------------------------------------------------------------------//
  //-----------------------------------------------------------------------------------------------------------------------------------------------------//

  function isPage(page) {
    var path = window.location.pathname;
    return path.includes(page);
  }

  function getSessionDependingOnPage() {
    var result = null;
    sessions.forEach(function(session) {
      if (isPage(session.page)) result = session;
    });
    return result;
  }

  function tagAndLinkKanji(char, guruedKanji) {
    var renderedChar = char;

    if (isKanji(char)) {
      renderedChar = isAtLeastGuru(char, guruedKanji)
        ? renderKanji(char, "guruedKanji")
        : renderKanji(char, "unguruedKanji");
    }

    return renderedChar;
  }
  /**
   * Determine if the character is a Kanji, inspired by https://stackoverflow.com/a/15034560
   */

  function isKanji(char) {
    return isCommonOrUncommonKanji(char) || isRareKanji(char);
  }

  function isCommonOrUncommonKanji(char) {
    return char >= "\u4E00" && char <= "\u9FAF";
  }

  function isRareKanji(char) {
    char >= "\u3400" && char <= "\u4DBF";
  }
  /**
   * Renders the link for a kanji you've gurued
   * Knji pages always use https://www.wanikani.com/kanji/{kanji} where {kanji} is the kanji character
   */

  function renderKanji(kanji, color) {
    return '<a href="https://www.wanikani.com/kanji/'
      .concat(kanji, '" target="_blank" class="')
      .concat(color, '" title="go to kanji page">')
      .concat(kanji, "</a>");
  }

  function isAtLeastGuru(char, guruedKanji) {
    if (!guruedKanji) return true;
    return guruedKanji.includes(char);
  }

  function getGuruedKanji() {
    return wkof.ItemData.get_items({
      wk_items: {
        filters: {
          item_type: ["kan"],
          srs: ["guru1", "guru2", "mast", "enli", "burn"]
        }
      }
    });
  }

  function extractKanjiFromResponse(items) {
    var kanjis = [];
    items.forEach(function(item) {
      kanjis.push(item.data.characters);
    });
    console.log(kanjis);
    return kanjis;
  }

  function createSource(type, sentence) {
    var source = document.createElement("source");
    source.setAttribute("type", type);
    source.setAttribute(
      "src",
      "https://translate.google.com/translate_tts?ie=UTF-8&client=tw-ob&tl=ja&total=1&idx=0&q=".concat(
        sentence
      )
    );
    return source;
  }

  function highlightKanji() {
    var gurued = document.querySelectorAll(
      ".context-sentence-group a.guruedKanji"
    );
    gurued.forEach(function(kanji) {
      kanji.setAttribute("style", "color: ".concat(state.guruedKanjiColor));
    });
    var ungurued = document.querySelectorAll(
      ".context-sentence-group a.unguruedKanji"
    );
    ungurued.forEach(function(kanji) {
      kanji.setAttribute("style", "color: ".concat(state.unguruedKanjiColor));
    });
  } // Neccessary in order for audio to work

  function createReferrer() {
    var remRef = document.createElement("meta");
    remRef.name = "referrer";
    remRef.content = "no-referrer";
    document.querySelector("head").append(remRef);
  }
})();