您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds example sentences from anime movies and shows for vocabulary from immersionkit.com
当前为
// ==UserScript== // @name Wanikani Anime Sentences 2 // @description Adds example sentences from anime movies and shows for vocabulary from immersionkit.com // @version 1.2.0 // @author psdcon, edited by Inserio // @namespace wkanimesentences/inserio // @match https://www.wanikani.com/* // @match https://preview.wanikani.com/* // @require https://gf.qytechs.cn/scripts/430565-wanikani-item-info-injector/code/WaniKani%20Item%20Info%20Injector.user.js?version=1343973 // @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: { playbackRate: 1, exampleLimit: 100, showEnglish: 'onhover', showJapanese: 'always', showFurigana: 'onhover', sentenceSorting: 'shortness', filterExactSearch: true, filterJLPTLevel: 0, filterWaniKaniLevel: true, // Enable all shows and movies by default filterAnimeShows: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true, 11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true, 19: true, 20: true, 21: true, 22: true, 23: true, 24: true, 25: true, 26: true, 27: true, 28: true, 29: true, 30: true, 31: true, 32: true, 33: true, 34: true, 35: true, 36: true, 37: true, 38: true, 39: true, 40: true, 41: true, 42: true, 43: true, 44: true, 45: true, 46: true, 47: true, 48: true, 49: true, 50: true, 51: true, 52: true, 53: true}, filterAnimeMovies: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true}, filterGhibli: {0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true, 11: true, 12: 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 and modified after testing a few example search results const animeShows = { 0: "Angel Beats!", 1: "Anohana the flower we saw that day", 2: "Assassination Classroom Season 1", 3: "Bakemonogatari", 4: "Bunny Drop", 5: "Boku no Hero Academia Season 1", 6: "Cardcaptor Sakura", 7: "Chobits", 8: "Clannad", 9: "Clannad After Story", 10: "Code Geass Season 1", 11: "Daily Lives of High School Boys", 12: "Death Note", 13: "Demon Slayer - Kimetsu no Yaiba", 14: "Durarara!!", 15: "Erased", 16: "Fairy Tail", 17: "Fate Stay Night Unlimited Blade Works", 18: "Fate Zero", 19: "From the New World", 20: "Fruits Basket Season 1", 21: "Fullmetal Alchemist Brotherhood", 22: "God's Blessing on this Wonderful World!", 23: "Haruhi Suzumiya", 24: "Hunter × Hunter", 25: "Hyouka", 26: "Is The Order a Rabbit", 27: "K-On!", 28: "Kakegurui", 29: "Kanon (2006)", 30: "Kill la Kill", 31: "Kino's Journey", 32: "Kokoro Connect", 33: "Little Witch Academia", 34: "Lucky Star", 35: "Mahou Shoujo Madoka Magica", 36: "Mononoke", 37: "My Little Sister Can't Be This Cute", 38: "New Game!", 39: "Nisekoi", 40: "No Game No Life", 41: "Noragami", 42: "One Week Friends", 43: "Psycho Pass", 44: "Re:Zero − Starting Life in Another World", 45: "ReLIFE", 46: "Shirokuma Cafe", 47: "Sound! Euphonium", 48: "Steins Gate", 49: "Sword Art Online", 50: "The Pet Girl of Sakurasou", 51: "Toradora!", 52: "Wandering Witch The Journey of Elaina", 53: "Your Lie in April", }; const animeMovies = { 0: "Only Yesterday", 1: "The Garden of Words", 2: "The Girl Who Leapt Through Time", 3: "The World God Only Knows", 4: "Weathering with You", 5: "Wolf Children", 6: "Your Name", }; const ghibliTitles = { 0: "Castle in the sky", 1: "From Up on Poppy Hill", 2: "Grave of the Fireflies", 3: "Howl's Moving Castle", 4: "Kiki's Delivery Service", 5: "My Neighbor Totoro", 6: "Princess Mononoke", 7: "Spirited Away", 8: "The Cat Returns", 9: "The Secret World of Arrietty", 10: "The Wind Rises", 11: "When Marnie Was There", 12: "Whisper of the Heart", }; main(); function main() { init(() => wkItemInfo.forType(`vocabulary,kanaVocabulary`).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 utilizes to provide the settings dialog for the script. You can still use ${scriptName} normally though` ); callback(); } } function getLevel() { wkof.Apiv2.fetch_endpoint('level_progressions', (window.unsafeWindow ?? window).options ?? analyticsOptions).then((response) => { state.userLevel = response.data[response.data.length - 1].data.level; }); } function onExamplesVisible(item) { state.item = item; // current vocab item addAnimeSentences(); } function addAnimeSentences() { const parentEl = document.createElement("div"); parentEl.setAttribute("id", 'anime-sentences-parent'); let header = ['Anime Sentences']; const settingsBtn = document.createElement("svg"); settingsBtn.setAttribute("style", "font-size: 14px; cursor: pointer; vertical-align: middle; margin-left: 10px;"); settingsBtn.textContent = '⚙️'; settingsBtn.onclick = openSettings; let sentencesEl = document.createElement("div"); sentencesEl.innerText = 'Loading...'; header.push(settingsBtn); parentEl.append(sentencesEl); state.sentencesEl = sentencesEl; if (state.item.injector) { if (state.item.on === 'lesson') { state.item.injector.appendAtTop(header, parentEl); } else { // itemPage, review state.item.injector.append(header, parentEl); } } fetchImmersionKitData().then(examples => { renderSentences(examples); }); } function fetchImmersionKitData() { let keyword = state.item.characters.replace('〜', ''); // for "counter" kanji if (state.settings.filterExactSearch) { keyword = `「${keyword}」`; } const tags = ''; // TODO: &tags= const sentenceSorting = state.settings.sentenceSorting !== 'none' ? `&sort=${state.settings.sentenceSorting}` : ''; const jlptFilter = state.settings.filterJLPTLevel !== 0 ? `&jlpt=${state.settings.filterJLPTLevel}` : ''; const wkLevelFilter = state.settings.filterWaniKaniLevel ? `&wk=${state.userLevel}` : ''; const url = `https://api.immersionkit.com/look_up_dictionary?keyword=${keyword}&category=anime${sentenceSorting}${tags}${jlptFilter}${wkLevelFilter}`; return fetch(url) .then(response => response.json()) .then(data => { return data.data[0].examples; }); } function getDesiredShows() { // Convert settings dictionaries to array of titles let titles = []; for (const [key, value] of Object.entries(state.settings.filterAnimeShows)) { if (value === true) { titles.push(animeShows[key]); } } for (const [key, value] of Object.entries(state.settings.filterAnimeMovies)) { if (value === true) { titles.push(animeMovies[key]); } } for (const [key, value] of Object.entries(state.settings.filterGhibli)) { if (value === true) { titles.push(ghibliTitles[key]); } } return titles; } function renderSentences(sentences) { // Called from immersionkit response, and on settings save const exampleLenBeforeFilter = sentences.length; // Exclude non-selected titles let desiredTitles = getDesiredShows(); let examples = []; for (let i=0; i<sentences.length; i++) { let ex = sentences[i]; if (desiredTitles.includes(ex.deck_name)) examples.push(ex); } // This is necessary until the API is fixed to allow the sorting preference to happen before the deck sorting switch (state.settings.sentenceSorting) { case 'shortness': examples.sort((a, b) => a.sentence.length - b.sentence.length); break; case 'longness': examples.sort((a, b) => b.sentence.length - a.sentence.length); break; } let showJapanese = state.settings.showJapanese; let showEnglish = state.settings.showEnglish; let showFurigana = state.settings.showFurigana; let playbackRate = state.settings.playbackRate; let html = ''; let exampleLimit = Math.min(examples.length, state.settings.exampleLimit); if (exampleLenBeforeFilter === 0) { html = 'No sentences found.'; } else if (examples.length === 0 && exampleLenBeforeFilter > 0) { // TODO show which titles have how many examples html = 'No sentences found for your selected movies & shows.'; } else { const keyword = state.item.characters.replace('〜', ''); for (let i = 0; i < exampleLimit; i++) { const example = examples[i]; let japaneseText; switch (state.settings.showFurigana) { case 'never': japaneseText = example.sentence.replace(keyword, `<keyword>${keyword}</keyword>`); break; default: japaneseText = new Furigana(example.sentence_with_furigana, keyword, ['keyword']).ReadingHtml; break; } 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} <span><button class="audio-btn audio-idle">🔈</button></span> </div> <div class="ja"> <span class="${showJapanese === 'onhover' ? 'show-on-hover' : ''} ${showFurigana === 'onhover' ? 'show-ruby-on-hover' : ''} ${showJapanese === 'onclick' ? 'show-on-click' : ''}">${japaneseText}</span> </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; const audioIdleClass = "audio-idle"; const audioPlayClass = "audio-play"; const animeSentencesContainer = document.querySelector("#anime-sentences-parent"); const audioButtons = document.querySelectorAll(".anime-example .audio-btn"); for (let i = 0; i < audioButtons.length; i++) { let audioContainer; const button = audioButtons[i]; const onPlay = () => {button.classList.replace(audioIdleClass,audioPlayClass);button.textContent='🔊';}; const onStop = () => {button.classList.replace(audioPlayClass,audioIdleClass);button.textContent='🔈';removeAudioElement(audioContainer);}; button.onclick = function () { if ((audioContainer = document.querySelector("#anime-sentences-parent audio")) !== null) { let prevSource = audioContainer.src; audioContainer.pause(); if (prevSource === examples[i].sound_url) { return; } } audioContainer = document.createElement("audio"); audioContainer.src = examples[i].sound_url; audioContainer.playbackRate = playbackRate; audioContainer.onplay = onPlay; audioContainer.onpause = onStop; audioContainer.onended = onStop; audioContainer.onabort = onStop; animeSentencesContainer.append(audioContainer); audioContainer.play(); }; } // Click anywhere plays the audio let exampleEls = document.querySelectorAll("#anime-sentences-parent .anime-example"); exampleEls.forEach((a) => { a.onclick = function () { let button = this.querySelector('.audio-btn'); button.click(); }; }); // Assigning onclick function to .show-on-click elements document.querySelectorAll(".show-on-click").forEach((a) => { a.onclick = function (e) { e.stopPropagation(); // prevent this click from triggering the audio to play a.classList.toggle('show-on-click'); }; }); } function removeAudioElement(element) { if (element === undefined || element === null) { return; } element.src = ""; element.remove(); } //--------------------------------------------------------------------------------------------------------------// //----------------------------------------------SETTINGS--------------------------------------------------------// //--------------------------------------------------------------------------------------------------------------// function loadSettings() { return wkof.Settings.load(scriptId, state.settings); } function processLoadedSettings() { state.settings = wkof.settings[scriptId]; } function openSettings(e) { e.stopPropagation(); let config = { script_id: scriptId, title: scriptName, on_save: updateSettings, content: { general: { type: "section", label: "General" }, sentenceSorting: { type: "dropdown", label: "Sentence Sorting Order", hover_tip: "", content: { none: "Anime Title (alphabetical)", shortness: "Shortest first", longness: "Longest first" }, default: state.settings.sentenceSorting }, exampleLimit: { type: "number", label: "Example Limit", step: 1, min: 1, hover_tip: "Limit the number of entries that may appear.", default: state.settings.exampleLimit }, 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 or click 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" }, filterExactSearch: { type: "checkbox", label: "Exact Search", hover_tip: "Text must match term exactly", default: state.settings.filterExactSearch }, filterGhibli: { type: "list", label: "Ghibli Movies", multi: true, size: 6, hover_tip: "Select which Studio Ghibli movies you'd like to see examples from.", default: state.settings.filterGhibli, content: ghibliTitles }, filterAnimeMovies: { type: "list", label: "Anime Movies", multi: true, size: 6, hover_tip: "Select which anime movies you'd like to see examples from.", default: state.settings.filterAnimeMovies, content: animeMovies }, filterAnimeShows: { type: "list", label: "Anime Shows", multi: true, size: 6, hover_tip: "Select which anime shows you'd like to see examples from.", default: state.settings.filterAnimeShows, content: animeShows }, filterJLPTLevel: { type: "dropdown", label: "JLPT Level", hover_tip: "Only show sentences matching a particular JLPT Level or easier.", content: { 0: "No Filter", 1: "N1", 2: "N2", 3: "N3", 4: "N4", 5: "N5" }, default: state.settings.filterJLPTLevel, }, filterWaniKaniLevel: { type: "checkbox", label: "WaniKani Level", hover_tip: "Only show sentences with maximum 1 word outside of your current WaniKani level.", default: state.settings.filterWaniKaniLevel, }, credits: { type: "section", label: "Powered by immersionkit.com" }, } }; 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]; fetchImmersionKitData().then(examples => { renderSentences(examples); }); } //--------------------------------------------------------------------------------------------------------------// //-----------------------------------------------STYLES---------------------------------------------------------// //--------------------------------------------------------------------------------------------------------------// function createStyle() { const style = document.createElement("style"); style.setAttribute("id", "anime-sentences-style"); // language=CSS style.innerHTML = ` #anime-sentences-parent > div { overflow-y: auto; max-height: 280px; } #anime-sentences-parent .fa-solid { border: none; font-size: 100%; } .anime-example { display: flex; align-items: center; margin-bottom: 1em; cursor: pointer; } .anime-example .audio-btn { background-color: transparent; } .anime-example .audio-btn.audio-idle { opacity: 50%; } /* Make text and background color the same to hide text */ .anime-example .anime-example-text .show-on-hover, .anime-example-text .show-on-click { background: #ccc; color: transparent; text-shadow: none; } .anime-example .anime-example-text .show-on-hover:hover { background: inherit; color: inherit } /* Color the keyword in the example sentence */ .anime-example .anime-example-text .ja span keyword { color: darkcyan; } /* Furigana hover */ .anime-example .anime-example-text .ja .show-ruby-on-hover ruby rt { visibility: hidden; } .anime-example .anime-example-text:hover .ja .show-ruby-on-hover ruby rt { visibility: visible; } .anime-example .title { font-weight: var(--font-weight-bold); } .anime-example .ja { font-size: var(--font-size-xlarge); } .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, emphasisText, tags) { var segments = ParseFurigana(reading || "", emphasisText, tags); 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, emphasisText, tags) { this.Expression = baseText; this.Hiragana = furigana.trim(); this.Reading = baseText + "[" + furigana + "]"; let openTags = ''; let closeTags = ''; if (tags !== null) { for (let i = 0; i < tags.length; i++) { openTags = `${openTags}<${tags[i]}>`; closeTags = `</${tags[i]}>${closeTags}`; } } this.ReadingHtml = "<ruby><rb>" + baseText.replace(emphasisText, `${openTags}${emphasisText}${closeTags}`) + "</rb><rt>" + furigana + "</rt></ruby>"; } function UndecoratedSegment(baseText) { this.Expression = baseText; this.Hiragana = baseText; this.Reading = baseText; this.ReadingHtml = baseText; } function ParseFurigana(reading, emphasisText, tags) { 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, emphasisText, tags); } 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; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址