您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhance the context sentence section, highlighting kanji and adding audio
"use strict"; // ==UserScript== // @name Advanced Context Sentence // @namespace https://openuserjs.org/users/abdullahalt // @version 1.40 // @description Enhance the context sentence section, highlighting kanji and adding audio // @author abdullahalt // @match https://www.wanikani.com/lesson/session // @match https://www.wanikani.com/review/session // @match https://www.wanikani.com/vocabulary/* // @grant none // @require https://unpkg.com/popper.js@1/dist/umd/popper.min.js // @require https://unpkg.com/tippy.js@4 // @copyright 2019, abdullahalt (https://openuserjs.org//users/abdullahalt) // @license MIT // ==/UserScript== // ==OpenUserJS== // @author abdullahalt // ==/OpenUserJS== (() => { //--------------------------------------------------------------------------------------------------------------// //-----------------------------------------------INITIALIZATION-------------------------------------------------// //--------------------------------------------------------------------------------------------------------------// const wkof = window.wkof; const scriptId = "AdvancedContextSentence"; const scriptName = "Advanced Context Sentence"; const vocabularyPage = "/vocabulary"; const recognizedSelector = "a.recognized"; const unrecognizedSelector = "a.unrecognized"; const sessions = [ { page: "/review/session", mount: "#item-info-col2", loading: "#loading", getHeader: sentences => { return sentences[0].previousElementSibling; } }, { page: "/lesson/session", mount: "#supplement-voc-context-sentence", loading: "#loading-screen", getHeader: sentences => { return sentences[0].parentElement.previousElementSibling; } } ]; let state = { settings: { recognizedKanjiColor: "#f100a1", unrecognizedKanjiColor: "#888888", recognitionLevel: "5", tooltip: { show: true, delay: 0, position: "top" } }, kanjis: [], jiff: false // JLPT, Joyo and Frequency Filters }; // Application start Point main(); function main() { // we don't need to observe any changes in the vocabulary page if (isPage(vocabularyPage)) { init(() => evolveContextSentence(sentences => { return sentences[0].previousElementSibling; }) ); return; } // Get the target for the session page to watch for changes const session = getSessionDependingOnPage(); if (session) startObserving(session); } function startObserving({ mount, loading, getHeader }) { const loadingObservationConfiguration = { attributes: true, childList: false, subtree: false }; const itemInfoObservationConfiguration = { attributes: false, childList: true, subtree: false }; const observeLoading = () => { observeChanges({ element: loading, config: loadingObservationConfiguration, onChange: runInit }); }; const runInit = () => { init(() => { observeSentenceChanges(); }); }; const observeSentenceChanges = () => { observeChanges({ element: mount, continuesObservation: true, config: itemInfoObservationConfiguration, onChange: () => evolve(), onInitObserver: () => evolve() }); }; const evolve = () => evolveContextSentence(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) { createReferrer(); createStyle(); if (wkof) { wkof.include("ItemData,Settings"); wkof .ready("ItemData,Settings") .then(loadSettings) .then(proccessLoadedSettings) .then(getKanji) .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(getHeader) { const sentences = document.querySelectorAll(".context-sentence-group"); if (sentences.length === 0) return; if (wkof) evolveHeader(getHeader(sentences)); sentences.forEach(sentence => { const japaneseSentence = sentence.querySelector('p[lang="ja"]'); const audioButton = createAudioButton(japaneseSentence.innerHTML); //let advancedExampleSentence = ""; const chars = japaneseSentence.innerHTML.split(""); japaneseSentence.innerHTML = ""; chars.forEach(char => { japaneseSentence.innerHTML = japaneseSentence.innerHTML + tagAndLinkKanji(char).trim(); }); highlightKanji(); japaneseSentence.append(audioButton); }); } function evolveHeader(header) { const 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; if (!header.querySelector("i.icon-gear")) header.append(settings); } /** * To fix a weird issue that occur in the session pages(where all audios play * if the audio for reading the word is clicked), * we have to create the audio element only for the time of palying the audio * and remove it afterward * @param {*} sentence */ function createAudioButton(sentence) { // contains audio and button as sibiling elements const audioContainer = document.createElement("span"); const mpegSource = createSource("audio/mpeg", sentence); const oogSource = createSource("audio/oog", sentence); const button = document.createElement("button"); button.setAttribute("class", "audio-btn audio-idle"); button.onclick = () => { if (audioContainer.childElementCount > 1) { const audio = audioContainer.querySelector("audio"); audio.pause(); button.setAttribute("class", "audio-btn audio-idle"); audio.remove(); return; } const audio = document.createElement("audio"); audio.setAttribute("display", "none"); audio.append(mpegSource, oogSource); audio.onplay = () => { button.setAttribute("class", "audio-btn audio-play"); }; audio.onended = () => { button.setAttribute("class", "audio-btn audio-idle"); audio.remove(); }; audioContainer.append(audio); audio.play(); }; audioContainer.append(button); return audioContainer; } function observeChanges(params) { const { element, config, onChange, onInitObserver = () => {}, continuesObservation = false } = params; 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(); const target = document.querySelector(element); const observer = new MutationObserver(() => { observer.disconnect(); onChange(); continuesObservation && observer.observe(target, config); }); observer.observe(target, config); } //--------------------------------------------------------------------------------------------------------------// //----------------------------------------------SETTINGS--------------------------------------------------------// //--------------------------------------------------------------------------------------------------------------// function loadSettings() { return wkof.Settings.load(scriptId, state.settings); } function proccessLoadedSettings() { state.settings = wkof.settings[scriptId]; } function openSettings() { var config = { script_id: scriptId, title: scriptName, on_save: updateSettings, content: { highlightColors: { type: "section", label: "Highlights" }, recognizedKanjiColor: { type: "color", label: "Recognized Kanji", hover_tip: "Kanji you should be able to recognize will be highlighted using this color", default: state.settings.recognizedKanjiColor }, unrecognizedKanjiColor: { type: "color", label: "Unrecognized Kanji", hover_tip: "Kanji you shouldn't be able to recognize will be highlighted using this color", default: state.settings.unrecognizedKanjiColor }, recognitionLevel: { type: "dropdown", label: "Recognition Level", hover_tip: "Any kanji with this level or higher will be highlighted with the 'Recognized Kanji' color", default: state.settings.recognitionLevel, content: { 1: stringfySrs(1), 2: stringfySrs(2), 3: stringfySrs(3), 4: stringfySrs(4), 5: stringfySrs(5), 6: stringfySrs(6), 7: stringfySrs(7), 8: stringfySrs(8), 9: stringfySrs(9) } }, tooltip: { type: "section", label: "Tooltip" }, show: { type: "checkbox", label: "Show Tooltip", hover_tip: "Display a tooltip when hovering on kanji that will display some of its properties", default: state.settings.tooltip.show, path: "@tooltip.show" }, delay: { type: "number", label: "Delay", hover_tip: "Delay in ms before the tooltip is shown", default: state.settings.tooltip.delay, path: "@tooltip.delay" }, position: { type: "dropdown", label: "Position", hover_tip: "The placement of the tooltip", default: state.settings.tooltip.position, path: "@tooltip.position", content: { top: "Top", bottom: "Bottom", right: "Right", left: "Left" } } } }; var 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]; highlightKanji(); } //---------------------------------------------------------------------------------------------------------------// //-------------------------------------------HELPER FUNCTIONS----------------------------------------------------// //---------------------------------------------------------------------------------------------------------------// function isPage(page) { const path = window.location.pathname; return path.includes(page); } function getSessionDependingOnPage() { let result = null; sessions.forEach(session => { if (isPage(session.page)) result = session; }); return result; } function tagAndLinkKanji(char) { return isKanji(char) ? wrapInAnchor(char).outerHTML : char; } /** * 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 the kanji * Knji pages always use https://www.wanikani.com/kanji/{kanji} where {kanji} is the kanji character */ function wrapInAnchor(char) { const anchor = document.createElement("a"); anchor.setAttribute("target", "_blank"); anchor.setAttribute("class", "recognized"); if (!wkof) { anchor.setAttribute("href", `https://www.wanikani.com/kanji/${char}`); anchor.innerText = char; return anchor; } const kanji = state.kanjis.find(item => item.char == char); anchor.setAttribute("data-srs", kanji ? kanji.srs : -1); anchor.setAttribute("data-kanji", char); anchor.setAttribute( "href", kanji ? kanji.url : `https://jisho.org/search/${char}` ); anchor.innerText = char; return anchor; } function createTooltip(kanji) { if (!wkof) { const container = document.createElement("span"); return container; } const container = document.createElement("div"); container.setAttribute("class", "acs-tooltip"); if (!kanji) { const span = document.createElement("span"); span.innerText = "Wanikani doesn't have this kanji! :("; container.append(span); return container; } const onyomis = kanji.readings.filter( item => item.type.toLocaleLowerCase() === "onyomi" ); const kunyomis = kanji.readings.filter( item => item.type.toLocaleLowerCase() === "kunyomi" ); const onyomi = stringfyArray(onyomis, item => item.reading); const kunyomi = stringfyArray(kunyomis, item => item.reading); const meaning = stringfyArray(kanji.meanings, item => item.meaning); container.append(generateInfo("LV", kanji.level)); container.append(generateInfo("EN", meaning)); if (onyomi !== "None" && onyomi !== "") container.append(generateInfo("ON", onyomi)); if (kunyomi !== "None" && kunyomi !== "") container.append(generateInfo("KN", kunyomi)); container.append(generateInfo("SRS", stringfySrs(kanji.srs))); if (state.jiff) { container.append(generateInfo("JOYO", kanji.joyo)); container.append(generateInfo("JLPT", kanji.jlpt)); container.append(generateInfo("FREQ", kanji.frequency)); } return container; } function stringfyArray(array, pathToString) { let stringfied = ""; array.forEach(item => { stringfied = stringfied.concat(pathToString(item) + ", "); }); stringfied = stringfied.substring(0, stringfied.length - 2); return stringfied; } function stringfySrs(srs) { switch (srs) { case -1: return "Locked"; case 0: return "Ready To Learn"; case 1: return "Apprentice 1"; case 2: return "Apprentice 2"; case 3: return "Apprentice 3"; case 4: return "Apprentice 4"; case 5: return "Guru 1"; case 6: return "Guru 2"; case 7: return "Master"; case 8: return "Enlightened"; case 9: return "Burned"; default: return ""; } } function generateInfo(title, info) { const container = document.createElement("div"); const key = document.createElement("span"); key.setAttribute("class", "acs-tooltip-header"); const value = document.createElement("span"); key.innerText = title; value.innerText = info; container.append(key, " ", value); return container; } function getKanji() { const filters = { item_type: ["kan"] }; if (wkof.get_state("wkof.Kumirei.JJFFilters") === "ready") { state.jiff = true; filters.include_frequency_data = true; filters.include_jlpt_data = true; filters.include_joyo_data = true; } else { console.warn( `${scriptName}: You don't have Open Framework JLPT Joyo and Frequency Filters by @Kumirei installed (version 0.1.4 or later). Install the script if you want to get more information while hovering on Kanji on Context Sentences. Script URL: https://community.wanikani.com/t/userscript-open-framework-jlpt-joyo-and-frequency-filters/35096` ); } return wkof.ItemData.get_items({ wk_items: { options: { assignments: true }, filters } }); } function extractKanjiFromResponse(items) { const kanjis = []; items.forEach(item => { const kanji = { char: item.data.characters, readings: item.data.readings, level: item.data.level, meanings: item.data.meanings, url: item.data.document_url, srs: item.assignments ? item.assignments.srs_stage : -1, jlpt: item.jlpt_level, joyo: item.joyo_grade, frequency: item.frequency }; kanjis.push(enhanceWithAditionalFilters(kanji, item)); }); state.kanjis = kanjis; } function enhanceWithAditionalFilters(kanji, item) { if (state.jiff) { kanji.jlpt = item.jlpt_level; kanji.joyo = item.joyo_grade; kanji.frequency = item.frequency; } return kanji; } function createSource(type, sentence) { const 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=${sentence}` ); return source; } function highlightKanji() { const rules = document.querySelector("#acs-style").sheet.cssRules; rules[0].style.color = state.settings.recognizedKanjiColor; rules[1].style.color = state.settings.unrecognizedKanjiColor; if (!wkof) return; const anchors = document.querySelectorAll(".context-sentence-group a"); anchors.forEach(anchor => { const srs = anchor.getAttribute("data-srs"); const char = anchor.getAttribute("data-kanji"); if (srs >= state.settings.recognitionLevel) anchor.setAttribute("class", "recognized"); else { anchor.setAttribute("class", "unrecognized"); } if (anchor._tippy) anchor._tippy.destroy(); if (state.settings.tooltip.show) { const kanji = state.kanjis.find(item => item.char == char); const tooltip = createTooltip(kanji); tippy(anchor, { content: tooltip.outerHTML, size: "small", arrow: true, placement: state.settings.tooltip.position, delay: [state.settings.tooltip.delay, 20] }); } }); } // Neccessary in order for audio to work function createReferrer() { const remRef = document.createElement("meta"); remRef.name = "referrer"; remRef.content = "no-referrer"; document.querySelector("head").append(remRef); } // Styles function createStyle() { const style = document.createElement("style"); style.setAttribute("id", "acs-style"); style.innerHTML = ` /* Kanji */ /* It's important for this one to be the first rule*/ ${recognizedSelector} { } /* It's important for this one to be the second rule*/ ${unrecognizedSelector} { } .context-sentence-group p a { text-decoration: none; } .context-sentence-group p a:hover { text-decoration: none; } .acs-tooltip { text-align: left } .acs-tooltip-header { color: #929292 } `; document.querySelector("head").append(style); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址