Skip to content

Instantly share code, notes, and snippets.

@memosiki
Last active November 5, 2021 08:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save memosiki/259836ad62818d106acd5a9408fbf590 to your computer and use it in GitHub Desktop.
Save memosiki/259836ad62818d106acd5a9408fbf590 to your computer and use it in GitHub Desktop.
Wanikani Anime Sentences.user.js
// ==UserScript==
// @name Wanikani Anime Sentences
// @description Adds example sentences from anime movies and shows for vocabulary
// @version 1.0.2
// @author psdcon
// @namespace wkanimesentences
// @include /^https://(www|preview).wanikani.com//
// @match https://www.wanikani.com/lesson/session
// @match https://www.wanikani.com/review/session
// @match https://www.wanikani.com/vocabulary/*
// @match https://preview.wanikani.com/lesson/session
// @match https://preview.wanikani.com/review/session
// @match https://preview.wanikani.com/vocabulary/*
// @require https://greasyfork.org/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=969075
// @copyright 2021+, Paul Connolly
// @license MIT; http://opensource.org/licenses/MIT
// @run-at document-end
// @grant none
// ==/UserScript==
(() => {
//--------------------------------------------------------------------------------------------------------------//
//-----------------------------------------------INITIALIZATION-------------------------------------------------//
//--------------------------------------------------------------------------------------------------------------//
const wkof = window.wkof;
const scriptId = "anime-sentences";
const scriptName = "Anime Sentences";
let state = {
settings: {
numExamples: 3,
playbackRate: 0.75,
showEnglish: 'onhover',
showJapanese: 'always',
showFurigana: 'onhover',
filterWaniKaniLevel: true,
filterGeneralAnime: {},
// All available Ghibli films enabled by default
filterGhibli: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true},
},
item: null, // current vocab from wkinfo
userLevel: '', // most recent level progression
immersionKitData: null, // cached so sentences can be re-rendered after settings change
sentencesEl: null, // referenced so sentences can be re-rendered after settings change
};
// Titles taken from https://www.immersionkit.com/information
const animeTitles = {
0: "Angel Beats!",
1: "Anohana the flower we saw that day",
2: "Assassination Classroom Season 1",
3: "Bakemonogatari",
4: "Boku no Hero Academia Season 1",
5: "Cardcaptor Sakura",
6: "Clannad",
7: "Code Geass Season 1",
8: "Death Note",
9: "Durarara!!",
10: "Erased",
11: "Fullmetal Alchemist Brotherhood",
12: "K-On!",
13: "Kanon (2006)",
14: "Kill la Kill",
15: "Kokoro Connect",
16: "Little Witch Academia",
17: "Mahou Shoujo Madoka Magica",
18: "No Game No Life",
19: "Noragami",
20: "Psycho Pass",
21: "Re Zero − Starting Life in Another World",
22: "Shirokuma Cafe",
23: "Steins Gate",
24: "Sword Art Online",
25: "The Garden of Words",
26: "The Girl Who Leapt Through Time",
27: "Wolf Children",
28: "Your Lie in April",
}
const ghibliTitles = {
0: "Castle in the sky",
1: "Howl's Moving Castle",
2: "Kiki's Delivery Service",
3: "My Neighbor Totoro",
4: "Spirited Away",
5: "The Cat Returns",
6: "The Secret World of Arrietty",
7: "The Wind Rises",
8: "When Marnie Was There",
}
main();
function main() {
init(() => wkItemInfo.forType(`vocabulary`).under(`examples`).notify(
(item) => onExamplesVisible(item))
);
}
function init(callback) {
createStyle();
if (wkof) {
wkof.include("ItemData,Settings");
wkof
.ready("Apiv2,Settings")
.then(loadSettings)
.then(processLoadedSettings)
.then(getLevel)
.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 dialog for the script. You can still use Advanced Context Sentence normally though`
);
callback();
}
}
function getLevel() {
wkof.Apiv2.fetch_endpoint('level_progressions', options).then((response) => {
state.userLevel = response.data[response.data.length - 1].data.level
});
}
function onExamplesVisible(item) {
state.item = item // current vocab item
addAnimeSentences()
}
function addAnimeSentences() {
let queryString = state.item.characters.replace('〜', ''); // for "counter" kanji
let parentEl = document.createElement("div");
parentEl.setAttribute("id", 'anime-sentences-parent')
let header = state.item.on !== 'itemPage' ? document.createElement("h2") : document.createElement("h3");
header.innerText = 'Anime Sentences'
const settingsBtn = document.createElement("i");
settingsBtn.setAttribute("class", "fa fa-gear");
settingsBtn.setAttribute("style", "font-size: 14px; cursor: pointer; vertical-align: middle; margin-left: 10px;");
settingsBtn.onclick = openSettings
let sentencesEl = document.createElement("div");
sentencesEl.innerText = 'Loading...'
header.append(settingsBtn)
parentEl.append(header)
parentEl.append(sentencesEl)
state.sentencesEl = sentencesEl
if (state.item.injector) {
if (state.item.on === 'lesson') {
state.item.injector.appendAtTop(null, parentEl)
} else { // itemPage, review
state.item.injector.append(null, parentEl)
}
}
const wkLevelFilter = state.settings.filterWaniKaniLevel ? state.userLevel : '';
let url = `https://api.immersionkit.com/look_up_dictionary?keyword=${queryString}&tags=&jlpt=&wk=${wkLevelFilter}&sort=None&category=anime`
fetch(url)
.then(response => response.json())
.then(data => {
let examples = data.data[0].examples
examples.sort((a,b) => b.sentence.length - a.sentence.length)
state.immersionKitData = examples
renderSentences()
});
}
function getDesiredShows() {
// Convert settings dictionaries to list of titles
let titles = []
for (const [key, value] of Object.entries(state.settings.filterGeneralAnime)) {
if (value === true) {
titles.push(animeTitles[key])
}
}
for (const [key, value] of Object.entries(state.settings.filterGhibli)) {
if (value === true) {
titles.push(ghibliTitles[key])
}
}
return titles
}
function renderSentences() {
// Called from immersionkit response, and on settings save
let examples = state.immersionKitData;
let desiredTitles = getDesiredShows()
examples = examples.filter(ex => desiredTitles.includes(ex.deck_name))
let showJapanese = state.settings.showJapanese;
let showEnglish = state.settings.showEnglish;
let showFurigana = state.settings.showFurigana;
let playbackRate = state.settings.playbackRate
let html = '';
if (examples.length === 0) {
html = 'No sentences found.'
} else {
let lim = Math.min(examples.length, state.settings.numExamples)
for (var i = 0; i < lim; i++) {
const example = examples[i]
let japaneseText = state.settings.showFurigana === 'never' ?
example.sentence :
new Furigana(example.sentence_with_furigana).ReadingHtml
html += `
<div class="anime-example">
<img src="${example.image_url}" alt="">
<div class="anime-example-text">
<div class="title" title="${example.id}">${example.deck_name}</div>
<div class="ja">
<span class="${showJapanese === 'onhover' ? 'show-on-hover' : ''} ${showFurigana === 'onhover' ? 'show-ruby-on-hover' : ''} ${showJapanese === 'onclick' ? 'show-on-click' : ''}">${japaneseText}</span>
<span><button class="audio-btn audio-idle"></button></span>
<audio src="${example.sound_url}"></audio>
</div>
<div class="en">
<span class="${showEnglish === 'onhover' ? 'show-on-hover' : ''} ${showEnglish === 'onclick' ? 'show-on-click' : ''}">${example.translation}</span>
</div>
</div>
</div>`
}
}
let sentencesEl = state.sentencesEl
sentencesEl.innerHTML = html
let audios = document.querySelectorAll(".anime-example audio")
audios.forEach((a) => {
a.playbackRate = playbackRate
let button = a.parentNode.querySelector('button')
a.onplay = () => {
button.setAttribute("class", "audio-btn audio-play")
};
a.onended = () => {
button.setAttribute('class', "audio-btn audio-idle")
};
})
// Click anywhere plays the audio
let exampleEls = document.querySelectorAll(".anime-example")
exampleEls.forEach((a) => {
a.onclick = function () {
let audio = this.querySelector('audio')
audio.play()
}
});
// Assing onclick function to .show-on-click elements
document.querySelectorAll(".show-on-click").forEach((a) => {
a.onclick = function () {
this.classList.toggle('show-on-click');
}
});
}
//--------------------------------------------------------------------------------------------------------------//
//----------------------------------------------SETTINGS--------------------------------------------------------//
//--------------------------------------------------------------------------------------------------------------//
function loadSettings() {
return wkof.Settings.load(scriptId, state.settings);
}
function processLoadedSettings() {
state.settings = wkof.settings[scriptId];
}
function openSettings() {
let config = {
script_id: scriptId,
title: scriptName,
on_save: updateSettings,
content: {
general: {
type: "section",
label: "General"
},
numExamples: {
type: "number",
label: "Number of Examples",
step: 1,
min: 1,
max: 10,
hover_tip: "The maximum number of sentences to show for each word.",
default: state.settings.numExamples
},
playbackRate: {
type: "number",
label: "Playback Speed",
step: 0.1,
min: 0.5,
max: 2,
hover_tip: "Speed to play back audio.",
default: state.settings.playbackRate
},
showJapanese: {
type: "dropdown",
label: "Show Japanese",
hover_tip: "When to show Japanese text. Hover enables transcribing a sentences first (play audio by clicking the image to avoid seeing the answer).",
content: {
always: "Always",
onhover: "On Hover",
onclick: "On Click",
},
default: state.settings.showJapanese
},
showFurigana: {
type: "dropdown",
label: "Show Furigana",
hover_tip: "These have been autogenerated so there may be mistakes.",
content: {
always: "Always",
onhover: "On Hover",
never: "Never",
},
default: state.settings.showFurigana
},
showEnglish: {
type: "dropdown",
label: "Show English",
hover_tip: "Hover allows testing your understanding before seeing the answer.",
content: {
always: "Always",
onhover: "On Hover",
onclick: "On Click",
},
default: state.settings.showEnglish
},
tooltip: {
type: "section",
label: "Filters"
},
filterWaniKaniLevel: {
type: "checkbox",
label: "WaniKani Level",
hover_tip: "Only show sentences appropriate for your current Wanikani level.",
default: state.settings.filterWaniKaniLevel,
},
filterGeneralAnime: {
type: "list",
label: "Anime",
multi: true,
size: 6,
hover_tip: "Select which anime you'd like to see examples from.",
default: state.settings.filterGeneralAnime,
content: animeTitles
},
filterGhibli: {
type: "list",
label: "Ghibli",
multi: true,
size: 6,
hover_tip: "Select which Studio Ghibli movies you'd like to see examples from.",
default: state.settings.filterGhibli,
content: ghibliTitles
}
}
};
let dialog = new wkof.Settings(config);
dialog.open();
}
// Called when the user clicks the Save button on the Settings dialog.
function updateSettings() {
state.settings = wkof.settings[scriptId];
renderSentences();
}
//--------------------------------------------------------------------------------------------------------------//
//-----------------------------------------------STYLES---------------------------------------------------------//
//--------------------------------------------------------------------------------------------------------------//
function createStyle() {
const style = document.createElement("style");
style.setAttribute("id", "anime-sentences-style");
style.innerHTML = `
.anime-example {
display: flex;
align-items: center;
margin-bottom: 1em;
cursor: pointer;
}
/* Make text and background color the same to hide text */
.anime-example-text .show-on-hover {
background: #ccc;
color: #ccc;
}
.anime-example-text .show-on-hover:hover {
background: inherit;
color: inherit
}
/* Furigana hover*/
.anime-example-text .show-ruby-on-hover ruby rt {
visibility: hidden;
}
.anime-example-text:hover .show-ruby-on-hover ruby rt {
visibility: visible;
}
.show-on-click {
background: #ccc;
color: #ccc;
}
.anime-example .title {
font-weight: 700;
}
.anime-example .ja {
font-size: 2em;
}
.anime-example img {
margin-right:1em;
max-width: 200px;
}
`;
document.querySelector("head").append(style);
}
//--------------------------------------------------------------------------------------------------------------//
//----------------------------------------------FURIGANA--------------------------------------------------------//
//--------------------------------------------------------------------------------------------------------------//
// https://raw.githubusercontent.com/helephant/Gem/master/src/Gem.Javascript/gem.furigana.js
function Furigana(reading) {
var segments = ParseFurigana(reading || "");
this.Reading = getReading();
this.Expression = getExpression();
this.Hiragana = getHiragana();
this.ReadingHtml = getReadingHtml();
function getReading() {
var reading = "";
for (var x = 0; x < segments.length; x++) {
reading += segments[x].Reading;
}
return reading.trim();
}
function getExpression() {
var expression = "";
for (var x = 0; x < segments.length; x++)
expression += segments[x].Expression;
return expression;
}
function getHiragana() {
var hiragana = "";
for (var x = 0; x < segments.length; x++) {
hiragana += segments[x].Hiragana;
}
return hiragana;
}
function getReadingHtml() {
var html = "";
for (var x = 0; x < segments.length; x++) {
html += segments[x].ReadingHtml;
}
return html;
}
}
function FuriganaSegment(baseText, furigana) {
this.Expression = baseText;
this.Hiragana = furigana.trim();
this.Reading = baseText + "[" + furigana + "]";
this.ReadingHtml = "<ruby><rb>" + baseText + "</rb><rt>" + furigana + "</rt></ruby>";
}
function UndecoratedSegment(baseText) {
this.Expression = baseText;
this.Hiragana = baseText;
this.Reading = baseText;
this.ReadingHtml = baseText;
}
function ParseFurigana(reading) {
var segments = [];
var currentBase = "";
var currentFurigana = "";
var parsingBaseSection = true;
var parsingHtml = false;
var characters = reading.split('');
while (characters.length > 0) {
var current = characters.shift();
if (current === '[') {
parsingBaseSection = false;
} else if (current === ']') {
nextSegment();
} else if (isLastCharacterInBlock(current, characters) && parsingBaseSection) {
currentBase += current;
nextSegment();
} else if (!parsingBaseSection)
currentFurigana += current;
else
currentBase += current;
}
nextSegment();
function nextSegment() {
if (currentBase)
segments.push(getSegment(currentBase, currentFurigana));
currentBase = "";
currentFurigana = "";
parsingBaseSection = true;
parsingHtml = false;
}
function getSegment(baseText, furigana) {
if (!furigana || furigana.trim().length === 0)
return new UndecoratedSegment(baseText);
return new FuriganaSegment(baseText, furigana);
}
function isLastCharacterInBlock(current, characters) {
return !characters.length ||
(isKanji(current) !== isKanji(characters[0]) && characters[0] !== '[');
}
function isKanji(character) {
return character && character.charCodeAt(0) >= 0x4e00 && character.charCodeAt(0) <= 0x9faf;
}
return segments;
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment