您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Embeds anime images & audio examples into JPDB review and vocabulary pages using Immersion Kit's API. Compatible only with TamperMonkey.
当前为
// ==UserScript== // @name JPDB Immersion Kit Examples // @version 1.5 // @description Embeds anime images & audio examples into JPDB review and vocabulary pages using Immersion Kit's API. Compatible only with TamperMonkey. // @author awoo // @namespace jpdb-immersion-kit-examples // @match https://jpdb.io/review* // @match https://jpdb.io/vocabulary/* // @match https://jpdb.io/kanji/* // @grant GM_addElement // @grant GM_xmlhttpRequest // @license MIT // ==/UserScript== (function() { 'use strict'; const CONFIG = { IMAGE_WIDTH: '400px', ENABLE_EXAMPLE_TRANSLATION: true, SENTENCE_FONT_SIZE: '120%', TRANSLATION_FONT_SIZE: '85%', COLORED_SENTENCE_TEXT: true, AUTO_PLAY_SOUND: true, SOUND_VOLUME: 0.8, NUM_PRELOADS: 1, WIDE_MODE: true, PAGE_MAX_WIDTH: '75rem' }; const state = { currentExampleIndex: 0, examples: [], apiDataFetched: false, vocab: '', embedAboveSubsectionMeanings: false, preloadedIndices: new Set(), currentAudio: null, exactSearch: true }; function getImmersionKitData(vocab, exactSearch, callback) { const searchVocab = exactSearch ? `「${vocab}」` : vocab; const url = `https://api.immersionkit.com/look_up_dictionary?keyword=${encodeURIComponent(searchVocab)}&sort=shortness`; GM_xmlhttpRequest({ method: "GET", url: url, onload: function(response) { if (response.status === 200) { try { const jsonData = JSON.parse(response.responseText); if (jsonData.data && jsonData.data[0] && jsonData.data[0].examples) { state.examples = jsonData.data[0].examples; state.apiDataFetched = true; } } catch (e) { console.error('Error parsing JSON response:', e); } } callback(); }, onerror: function(error) { console.error('Error fetching data:', error); callback(); } }); } function getStoredData(key) { const storedValue = localStorage.getItem(key); if (storedValue) { const [index, exactState] = storedValue.split(','); return { index: parseInt(index, 10), exactState: exactState === '1' }; } return { index: 0, exactState: state.exactSearch }; } function storeData(key, index, exactState) { const value = `${index},${exactState ? 1 : 0}`; localStorage.setItem(key, value); } function exportFavorites() { const favorites = {}; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); favorites[key] = localStorage.getItem(key); } const blob = new Blob([JSON.stringify(favorites, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'favorites.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function importFavorites(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const favorites = JSON.parse(e.target.result); for (const key in favorites) { localStorage.setItem(key, favorites[key]); } alert('Favorites imported successfully!'); } catch (error) { alert('Error importing favorites:', error); } }; reader.readAsText(file); } function parseVocabFromAnswer() { const elements = document.querySelectorAll('a[href*="/kanji/"], a[href*="/vocabulary/"]'); for (const element of elements) { const href = element.getAttribute('href'); const text = element.textContent.trim(); const match = href.match(/\/(kanji|vocabulary)\/(?:\d+\/)?([^\#]*)#/); if (match) return match[2].trim(); if (text) return text.trim(); } return ''; } function parseVocabFromReview() { const kindElement = document.querySelector('.kind'); if (!kindElement) return ''; const kindText = kindElement.textContent.trim(); if (kindText !== 'Kanji' && kindText !== 'Vocabulary') return ''; if (kindText === 'Vocabulary') { const plainElement = document.querySelector('.plain'); if (!plainElement) return ''; let vocabulary = plainElement.textContent.trim(); const nestedVocabularyElement = plainElement.querySelector('div:not([style])'); if (nestedVocabularyElement) { vocabulary = nestedVocabularyElement.textContent.trim(); } const specificVocabularyElement = plainElement.querySelector('div:nth-child(3)'); if (specificVocabularyElement) { vocabulary = specificVocabularyElement.textContent.trim(); } const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/; if (kanjiRegex.test(vocabulary) || vocabulary) { return vocabulary; } } else if (kindText === 'Kanji') { const hiddenInput = document.querySelector('input[name="c"]'); if (!hiddenInput) return ''; const vocab = hiddenInput.value.split(',')[1]; const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/; if (kanjiRegex.test(vocab)) { return vocab; } } return ''; } function parseVocabFromVocabulary() { const url = window.location.href; const match = url.match(/https:\/\/jpdb\.io\/vocabulary\/(\d+)\/([^\#]*)#a/); if (match) { let vocab = match[2]; state.embedAboveSubsectionMeanings = true; vocab = vocab.split('/')[0]; return decodeURIComponent(vocab); } return ''; } function parseVocabFromKanji() { const url = window.location.href; const match = url.match(/https:\/\/jpdb\.io\/kanji\/([^#]*)#a/); if (match) { return decodeURIComponent(match[1]); } return ''; } function highlightVocab(sentence, vocab) { if (!CONFIG.COLORED_SENTENCE_TEXT) return sentence; if (state.exactSearch) { const regex = new RegExp(`(${vocab})`, 'g'); return sentence.replace(regex, '<span style="color: var(--outline-input-color);">$1</span>'); } else { return vocab.split('').reduce((acc, char) => { const regex = new RegExp(char, 'g'); return acc.replace(regex, `<span style="color: var(--outline-input-color);">${char}</span>`); }, sentence); } } function createIconLink(iconClass, onClick, marginLeft = '0') { const link = document.createElement('a'); link.href = '#'; link.style.border = '0'; link.style.display = 'inline-flex'; link.style.verticalAlign = 'middle'; link.style.marginLeft = marginLeft; const icon = document.createElement('i'); icon.className = iconClass; icon.style.fontSize = '1.4rem'; icon.style.opacity = '1.0'; icon.style.verticalAlign = 'baseline'; icon.style.color = '#3d81ff'; link.appendChild(icon); link.addEventListener('click', onClick); return link; } function createQuoteButton() { const link = document.createElement('a'); link.href = '#'; link.style.border = '0'; link.style.display = 'inline-flex'; link.style.verticalAlign = 'middle'; link.style.marginLeft = '0rem'; const quoteIcon = document.createElement('span'); quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』'; quoteIcon.style.fontSize = '1.1rem'; quoteIcon.style.color = '#3D8DFF'; quoteIcon.style.verticalAlign = 'middle'; quoteIcon.style.position = 'relative'; quoteIcon.style.top = '0px'; link.appendChild(quoteIcon); link.addEventListener('click', (event) => { event.preventDefault(); state.exactSearch = !state.exactSearch; quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』'; const storedData = getStoredData(state.vocab); if (storedData && storedData.exactState === state.exactSearch) { state.currentExampleIndex = storedData.index; } else { state.currentExampleIndex = 0; } getImmersionKitData(state.vocab, state.exactSearch, () => { embedImageAndPlayAudio(); }); }); return link; } function createStarLink() { const link = document.createElement('a'); link.href = '#'; link.style.border = '0'; link.style.display = 'inline-flex'; link.style.verticalAlign = 'middle'; const storedValue = localStorage.getItem(state.vocab); const starIcon = document.createElement('span'); if (!storedValue) { starIcon.textContent = '☆'; } else { const [storedIndex, storedExactState] = storedValue.split(','); const index = parseInt(storedIndex, 10); const exactState = storedExactState === '1'; starIcon.textContent = (state.currentExampleIndex === index && state.exactSearch === exactState) ? '★' : '☆'; } starIcon.style.fontSize = '1.4rem'; starIcon.style.marginLeft = '0.5rem'; starIcon.style.color = '#3D8DFF'; starIcon.style.verticalAlign = 'middle'; starIcon.style.position = 'relative'; starIcon.style.top = '-2px'; link.appendChild(starIcon); link.addEventListener('click', (event) => { event.preventDefault(); const storedValue = localStorage.getItem(state.vocab); if (storedValue) { const [storedIndex, storedExactState] = storedValue.split(','); const index = parseInt(storedIndex, 10); const exactState = storedExactState === '1'; if (index === state.currentExampleIndex && exactState === state.exactSearch) { localStorage.removeItem(state.vocab); } else { localStorage.setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`); } } else { localStorage.setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`); } // Refresh the embed without playing the audio renderImageAndPlayAudio(state.vocab, false); }); return link; } function createExportImportButtons() { const exportButton = document.createElement('button'); exportButton.textContent = 'Export Favorites'; exportButton.style.marginRight = '10px'; exportButton.addEventListener('click', exportFavorites); const importButton = document.createElement('button'); importButton.textContent = 'Import Favorites'; importButton.addEventListener('click', () => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'application/json'; fileInput.addEventListener('change', importFavorites); fileInput.click(); }); const buttonContainer = document.createElement('div'); buttonContainer.style.textAlign = 'center'; buttonContainer.style.marginTop = '10px'; buttonContainer.append(exportButton, importButton); document.body.appendChild(buttonContainer); } function playAudio(soundUrl) { if (soundUrl) { // Stop the current audio if it's playing if (state.currentAudio) { state.currentAudio.pause(); state.currentAudio.src = ''; } // Fetch the audio file as a blob and create a blob URL GM_xmlhttpRequest({ method: 'GET', url: soundUrl, responseType: 'blob', onload: function(response) { const blobUrl = URL.createObjectURL(response.response); const audioElement = new Audio(blobUrl); audioElement.volume = CONFIG.SOUND_VOLUME; // Adjust volume as needed audioElement.play(); state.currentAudio = audioElement; // Keep reference to the current audio }, onerror: function(error) { console.error('Error fetching audio:', error); } }); } } function renderImageAndPlayAudio(vocab, shouldAutoPlaySound) { const example = state.examples[state.currentExampleIndex] || {}; const imageUrl = example.image_url || null; const soundUrl = example.sound_url || null; const sentence = example.sentence || null; const resultVocabularySection = document.querySelector('.result.vocabulary'); const hboxWrapSection = document.querySelector('.hbox.wrap'); const subsectionMeanings = document.querySelector('.subsection-meanings'); const subsectionComposedOfKanji = document.querySelector('.subsection-composed-of-kanji'); const subsectionPitchAccent = document.querySelector('.subsection-pitch-accent'); const subsectionLabels = document.querySelectorAll('h6.subsection-label'); const vboxGap = document.querySelector('.vbox.gap'); // Remove any existing container before creating a new one const existingContainer = document.getElementById('immersion-kit-container'); if (existingContainer) { existingContainer.remove(); } if (resultVocabularySection || hboxWrapSection || subsectionMeanings || subsectionLabels.length >= 3) { const wrapperDiv = document.createElement('div'); wrapperDiv.id = 'image-wrapper'; wrapperDiv.style.textAlign = 'center'; wrapperDiv.style.padding = '5px 0'; const textDiv = document.createElement('div'); textDiv.style.marginBottom = '5px'; textDiv.style.lineHeight = '1.4rem'; const contentText = document.createElement('span'); contentText.textContent = 'Immersion Kit'; contentText.style.color = 'var(--subsection-label-color)'; contentText.style.fontSize = '85%'; contentText.style.marginRight = '0.5rem'; contentText.style.verticalAlign = 'middle'; const speakerLink = createIconLink('ti ti-volume', (event) => { event.preventDefault(); playAudio(soundUrl); }, '0.5rem'); const starLink = createStarLink(); const quoteButton = createQuoteButton(); // Create the quote button textDiv.append(contentText, speakerLink, starLink, quoteButton); // Append the quote button wrapperDiv.appendChild(textDiv); if (imageUrl) { const imageElement = GM_addElement(wrapperDiv, 'img', { src: imageUrl, alt: 'Embedded Image', style: `max-width: ${CONFIG.IMAGE_WIDTH}; margin-top: 10px; cursor: pointer;` }); if (imageElement) { imageElement.addEventListener('click', () => { speakerLink.click(); }); } if (sentence) { const sentenceText = document.createElement('div'); sentenceText.innerHTML = highlightVocab(sentence, vocab); sentenceText.style.marginTop = '10px'; sentenceText.style.fontSize = CONFIG.SENTENCE_FONT_SIZE; sentenceText.style.color = 'lightgray'; sentenceText.style.maxWidth = CONFIG.IMAGE_WIDTH; sentenceText.style.whiteSpace = 'pre-wrap'; wrapperDiv.appendChild(sentenceText); if (CONFIG.ENABLE_EXAMPLE_TRANSLATION && example.translation) { const translationText = document.createElement('div'); translationText.innerHTML = replaceSpecialCharacters(example.translation); translationText.style.marginTop = '5px'; translationText.style.fontSize = CONFIG.TRANSLATION_FONT_SIZE; translationText.style.color = 'var(--subsection-label-color)'; translationText.style.maxWidth = CONFIG.IMAGE_WIDTH; translationText.style.whiteSpace = 'pre-wrap'; wrapperDiv.appendChild(translationText); } } else { const noneText = document.createElement('div'); noneText.textContent = 'None'; noneText.style.marginTop = '10px'; noneText.style.fontSize = '85%'; noneText.style.color = 'var(--subsection-label-color)'; wrapperDiv.appendChild(noneText); } } // Create a fixed-width container for arrows and image const navigationDiv = document.createElement('div'); navigationDiv.id = 'immersion-kit-embed'; navigationDiv.style.display = 'flex'; navigationDiv.style.justifyContent = 'center'; navigationDiv.style.alignItems = 'center'; navigationDiv.style.maxWidth = CONFIG.IMAGE_WIDTH; navigationDiv.style.margin = '0 auto'; const leftArrow = document.createElement('button'); leftArrow.textContent = '<'; leftArrow.style.marginRight = '10px'; leftArrow.disabled = state.currentExampleIndex === 0; leftArrow.addEventListener('click', () => { if (state.currentExampleIndex > 0) { state.currentExampleIndex--; renderImageAndPlayAudio(vocab, shouldAutoPlaySound); preloadImages(); } }); const rightArrow = document.createElement('button'); rightArrow.textContent = '>'; rightArrow.style.marginLeft = '10px'; rightArrow.disabled = state.currentExampleIndex >= state.examples.length - 1; rightArrow.addEventListener('click', () => { if (state.currentExampleIndex < state.examples.length - 1) { state.currentExampleIndex++; renderImageAndPlayAudio(vocab, shouldAutoPlaySound); preloadImages(); } }); const embedWrapper = document.createElement('div'); embedWrapper.style.flex = '0 0 auto'; embedWrapper.style.marginLeft = '10px'; embedWrapper.appendChild(navigationDiv); const containerDiv = document.createElement('div'); containerDiv.id = 'immersion-kit-container'; // Added ID for targeting containerDiv.style.display = 'flex'; containerDiv.style.alignItems = 'center'; containerDiv.style.justifyContent = CONFIG.WIDE_MODE ? 'flex-start' : 'center'; // Center if wide_mode is false containerDiv.append(leftArrow, wrapperDiv, rightArrow, embedWrapper); if (CONFIG.WIDE_MODE && subsectionMeanings) { const wrapper = document.createElement('div'); wrapper.style.display = 'flex'; wrapper.style.alignItems = 'flex-start'; const originalContentWrapper = document.createElement('div'); originalContentWrapper.style.flex = '1'; originalContentWrapper.appendChild(subsectionMeanings); // Append the new subsections with a newline before each if (subsectionComposedOfKanji) { const newline1 = document.createElement('br'); originalContentWrapper.appendChild(newline1); originalContentWrapper.appendChild(subsectionComposedOfKanji); } if (subsectionPitchAccent) { const newline2 = document.createElement('br'); originalContentWrapper.appendChild(newline2); originalContentWrapper.appendChild(subsectionPitchAccent); } wrapper.appendChild(originalContentWrapper); wrapper.appendChild(containerDiv); if (vboxGap) { // Remove only the dynamically added div under vboxGap const existingDynamicDiv = vboxGap.querySelector('#dynamic-content'); if (existingDynamicDiv) { existingDynamicDiv.remove(); } const dynamicDiv = document.createElement('div'); dynamicDiv.id = 'dynamic-content'; dynamicDiv.appendChild(wrapper); // Insert after the first child if the URL contains vocabulary if (window.location.href.includes('vocabulary')) { vboxGap.insertBefore(dynamicDiv, vboxGap.children[1]); } else { vboxGap.insertBefore(dynamicDiv, vboxGap.firstChild); // Insert at the top } } } else { if (state.embedAboveSubsectionMeanings && subsectionMeanings) { subsectionMeanings.parentNode.insertBefore(containerDiv, subsectionMeanings); } else if (resultVocabularySection) { resultVocabularySection.parentNode.insertBefore(containerDiv, resultVocabularySection); } else if (hboxWrapSection) { hboxWrapSection.parentNode.insertBefore(containerDiv, hboxWrapSection); } else if (subsectionLabels.length >= 4) { subsectionLabels[3].parentNode.insertBefore(containerDiv, subsectionLabels[3]); } } } if (CONFIG.AUTO_PLAY_SOUND && shouldAutoPlaySound) { playAudio(soundUrl); } } function embedImageAndPlayAudio() { const existingNavigationDiv = document.getElementById('immersion-kit-embed'); if (existingNavigationDiv) existingNavigationDiv.remove(); const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/; if (state.vocab && !state.apiDataFetched) { getImmersionKitData(state.vocab, state.exactSearch, () => { renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href)); preloadImages(); }); } else { renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href)); preloadImages(); } } function replaceSpecialCharacters(text) { return text.replace(/<br>/g, '\n').replace(/"/g, '"').replace(/\n/g, '<br>'); } function preloadImages() { const preloadDiv = GM_addElement(document.body, 'div', { style: 'display: none;' }); for (let i = Math.max(0, state.currentExampleIndex - CONFIG.NUM_PRELOADS); i <= Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUM_PRELOADS); i++) { if (!state.preloadedIndices.has(i)) { const example = state.examples[i]; if (example.image_url) { GM_addElement(preloadDiv, 'img', { src: example.image_url }); state.preloadedIndices.add(i); } } } } function onUrlChange() { state.embedAboveSubsectionMeanings = false; if (window.location.href.includes('/vocabulary/')) { state.vocab = parseVocabFromVocabulary(); } else if (window.location.href.includes('c=')) { state.vocab = parseVocabFromAnswer(); } else if (window.location.href.includes('/kanji/')) { state.vocab = parseVocabFromKanji(); } else { state.vocab = parseVocabFromReview(); } const storedData = getStoredData(state.vocab); state.currentExampleIndex = storedData.index; state.exactSearch = storedData.exactState; const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/; const shouldAutoPlaySound = !reviewUrlPattern.test(window.location.href); if (state.vocab) { embedImageAndPlayAudio(); } } const observer = new MutationObserver(() => { if (window.location.href !== observer.lastUrl) { observer.lastUrl = window.location.href; onUrlChange(); setPageWidth(); } }); function setPageWidth(){ if (CONFIG.WIDE_MODE) { document.body.style.maxWidth = CONFIG.PAGE_MAX_WIDTH; } }; observer.lastUrl = window.location; observer.lastUrl = window.location.href; observer.observe(document, { subtree: true, childList: true }); window.addEventListener('load', onUrlChange); window.addEventListener('popstate', onUrlChange); window.addEventListener('hashchange', onUrlChange); setPageWidth(); createExportImportButtons(); })();