您需要先安装一个扩展,例如 篡改猴、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.8 // @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', WIDE_MODE: true, PAGE_WIDTH: '75rem', SOUND_VOLUME: 80, ENABLE_EXAMPLE_TRANSLATION: true, SENTENCE_FONT_SIZE: '120%', TRANSLATION_FONT_SIZE: '85%', COLORED_SENTENCE_TEXT: true, AUTO_PLAY_SOUND: true, NUMBER_OF_PRELOADS: 1, MINIMUM_EXAMPLE_LENGTH: 0 }; const state = { currentExampleIndex: 0, examples: [], apiDataFetched: false, vocab: '', embedAboveSubsectionMeanings: false, preloadedIndices: new Set(), currentAudio: null, exactSearch: true }; //API FUNCTIONS===================================================================================================================== const MAX_ENTRIES = 1000; const EXPIRATION_TIME = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds function openIndexedDB() { return new Promise((resolve, reject) => { const request = indexedDB.open('ImmersionKitDB', 1); request.onupgradeneeded = function(event) { const db = event.target.result; if (!db.objectStoreNames.contains('dataStore')) { db.createObjectStore('dataStore', { keyPath: 'keyword' }); } }; request.onsuccess = function(event) { resolve(event.target.result); }; request.onerror = function(event) { reject('IndexedDB error: ' + event.target.errorCode); }; }); } function getFromIndexedDB(db, keyword) { return new Promise((resolve, reject) => { const transaction = db.transaction(['dataStore'], 'readonly'); const store = transaction.objectStore('dataStore'); const request = store.get(keyword); request.onsuccess = function(event) { const result = event.target.result; if (result && Date.now() - result.timestamp < EXPIRATION_TIME) { resolve(result.data); } else { resolve(null); } }; request.onerror = function(event) { reject('IndexedDB get error: ' + event.target.errorCode); }; }); } function saveToIndexedDB(db, keyword, data) { return new Promise(async (resolve, reject) => { const transaction = db.transaction(['dataStore'], 'readwrite'); const store = transaction.objectStore('dataStore'); // Check and delete old entries if necessary const allEntries = store.getAll(); allEntries.onsuccess = function(event) { const entries = event.target.result; if (entries.length >= MAX_ENTRIES) { // Sort entries by timestamp and delete oldest entries.sort((a, b) => a.timestamp - b.timestamp); const oldEntries = entries.slice(0, entries.length - MAX_ENTRIES + 1); const deletePromises = oldEntries.map(entry => { const deleteTransaction = db.transaction(['dataStore'], 'readwrite'); const deleteStore = deleteTransaction.objectStore('dataStore'); return new Promise((resolve, reject) => { const request = deleteStore.delete(entry.keyword); request.onsuccess = () => resolve(); request.onerror = () => reject('Failed to delete old entry'); }); }); Promise.all(deletePromises).then(() => { // Save the new entry after cleanup const request = store.put({ keyword: keyword, data: data, timestamp: Date.now() }); request.onsuccess = function(event) { resolve(); }; request.onerror = function(event) { reject('IndexedDB save error: ' + event.target.errorCode); }; }).catch(reject); } else { // Save the new entry const request = store.put({ keyword: keyword, data: data, timestamp: Date.now() }); request.onsuccess = function(event) { resolve(); }; request.onerror = function(event) { reject('IndexedDB save error: ' + event.target.errorCode); }; } }; allEntries.onerror = function(event) { reject('Failed to retrieve all entries'); }; }); } function getImmersionKitData(vocab, exactSearch) { return new Promise(async (resolve, reject) => { const searchVocab = exactSearch ? `「${vocab}」` : vocab; const url = `https://api.immersionkit.com/look_up_dictionary?keyword=${encodeURIComponent(searchVocab)}&sort=shortness&min_length=${CONFIG.MINIMUM_EXAMPLE_LENGTH}`; try { const db = await openIndexedDB(); const cachedData = await getFromIndexedDB(db, searchVocab); // Ensure cachedData is valid before accessing it if (cachedData && cachedData.data && Array.isArray(cachedData.data) && cachedData.data.length > 0) { console.log('Data retrieved from IndexedDB'); state.examples = cachedData.data[0].examples; state.apiDataFetched = true; resolve(); } else { console.log(`Calling API for: ${searchVocab}`); GM_xmlhttpRequest({ method: "GET", url: url, onload: async function(response) { if (response.status === 200) { const jsonData = parseJSON(response.responseText); console.log("API JSON Received"); console.log(url); if (validateApiResponse(jsonData)) { state.examples = jsonData.data[0].examples; state.apiDataFetched = true; await saveToIndexedDB(db, searchVocab, jsonData); resolve(); } else { reject('Invalid API response'); } } else { reject(`API call failed with status: ${response.status}`); } }, onerror: function(error) { reject(`An error occurred: ${error}`); } }); } } catch (error) { reject(`Error: ${error}`); } }); } function parseJSON(responseText) { try { return JSON.parse(responseText); } catch (e) { console.error('Error parsing JSON:', e); return null; } } function validateApiResponse(jsonData) { return jsonData && jsonData.data && jsonData.data[0] && jsonData.data[0].examples; } //FAVORITE DATA FUNCTIONS===================================================================================================================== 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); } //PARSE VOCAB FUNCTIONS===================================================================================================================== function parseVocabFromAnswer() { const elements = document.querySelectorAll('a[href*="/kanji/"], a[href*="/vocabulary/"]'); console.log("Parsing Answer Page"); 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'); console.log("Parsing Review Page"); 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) { console.log("Found 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)) { console.log("Found Kanji:", vocab); return vocab; } } return ''; } function parseVocabFromVocabulary() { const url = window.location.href; const match = url.match(/https:\/\/jpdb\.io\/vocabulary\/(\d+)\/([^\#]*)#a/); console.log("Parsing Vocabulary Page"); 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\/(\d+)\/([^\#]*)#a/); console.log("Parsing Kanji Page"); if (match) { let kanji = match[2]; state.embedAboveSubsectionMeanings = true; kanji = kanji.split('/')[0]; return decodeURIComponent(kanji); } return ''; } //EMBED FUNCTIONS===================================================================================================================== function createAnchor(marginLeft) { const anchor = document.createElement('a'); anchor.href = '#'; anchor.style.border = '0'; anchor.style.display = 'inline-flex'; anchor.style.verticalAlign = 'middle'; anchor.style.marginLeft = marginLeft; return anchor; } function createIcon(iconClass, fontSize = '1.4rem', color = '#3d81ff') { const icon = document.createElement('i'); icon.className = iconClass; icon.style.fontSize = fontSize; icon.style.opacity = '1.0'; icon.style.verticalAlign = 'baseline'; icon.style.color = color; return icon; } function createSpeakerButton(soundUrl) { const anchor = createAnchor('0.5rem'); const icon = createIcon('ti ti-volume'); anchor.appendChild(icon); anchor.addEventListener('click', (event) => { event.preventDefault(); playAudio(soundUrl); }); return anchor; } function createStarButton() { const anchor = createAnchor('0.5rem'); const starIcon = document.createElement('span'); const storedValue = localStorage.getItem(state.vocab); 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.color = '#3D8DFF'; starIcon.style.verticalAlign = 'middle'; starIcon.style.position = 'relative'; starIcon.style.top = '-2px'; anchor.appendChild(starIcon); anchor.addEventListener('click', (event) => { event.preventDefault(); toggleStarState(starIcon); }); return anchor; } function toggleStarState(starIcon) { 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); starIcon.textContent = '☆'; } else { localStorage.setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`); starIcon.textContent = '★'; } } else { localStorage.setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`); starIcon.textContent = '★'; } } function createQuoteButton() { const anchor = createAnchor('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'; anchor.appendChild(quoteIcon); anchor.addEventListener('click', (event) => { event.preventDefault(); toggleQuoteState(quoteIcon); }); return anchor; } function toggleQuoteState(quoteIcon) { 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; } state.apiDataFetched = false; // Reset the flag to allow API call getImmersionKitData(state.vocab, state.exactSearch) .then(() => { embedImageAndPlayAudio(); }) .catch(error => { console.error(error); }); } function createMenuButton() { const anchor = createAnchor('0.5rem'); const menuIcon = document.createElement('span'); menuIcon.innerHTML = '☰'; menuIcon.style.fontSize = '1.4rem'; menuIcon.style.color = '#3D8DFF'; menuIcon.style.verticalAlign = 'middle'; menuIcon.style.position = 'relative'; menuIcon.style.top = '-2px'; anchor.appendChild(menuIcon); anchor.addEventListener('click', (event) => { event.preventDefault(); const overlay = createOverlayMenu(); document.body.appendChild(createOverlayMenu()); }); return anchor; } function createTextButton(vocab, exact) { const textButton = document.createElement('a'); textButton.textContent = 'Immersion Kit'; textButton.style.color = 'var(--subsection-label-color)'; textButton.style.fontSize = '85%'; textButton.style.marginRight = '0.5rem'; textButton.style.verticalAlign = 'middle'; textButton.href = `https://www.immersionkit.com/dictionary?keyword=${encodeURIComponent(vocab)}&sort=shortness${exact ? '&exact=true' : ''}`; textButton.target = '_blank'; return textButton; } function createButtonContainer(soundUrl, vocab, exact) { const buttonContainer = document.createElement('div'); buttonContainer.className = 'button-container'; buttonContainer.style.display = 'flex'; buttonContainer.style.justifyContent = 'space-between'; buttonContainer.style.alignItems = 'center'; buttonContainer.style.marginBottom = '5px'; buttonContainer.style.lineHeight = '1.4rem'; const menuButton = createMenuButton(); const textButton = createTextButton(vocab, exact); const speakerButton = createSpeakerButton(soundUrl); const starButton = createStarButton(); const quoteButton = createQuoteButton(); const centeredButtonsWrapper = document.createElement('div'); centeredButtonsWrapper.style.display = 'flex'; centeredButtonsWrapper.style.justifyContent = 'center'; centeredButtonsWrapper.style.flex = '1'; centeredButtonsWrapper.append(textButton, speakerButton, starButton, quoteButton); buttonContainer.append(centeredButtonsWrapper, menuButton); return buttonContainer; } function stopCurrentAudio() { if (state.currentAudio) { state.currentAudio.pause(); state.currentAudio.removeAttribute('src'); state.currentAudio.load(); } } function playAudio(soundUrl) { if (soundUrl) { stopCurrentAudio(); 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 / 100); audioElement.play(); state.currentAudio = audioElement; }, 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; removeExistingContainer(); if (shouldRenderContainer()) { const wrapperDiv = createWrapperDiv(); const textDiv = createButtonContainer(soundUrl, vocab, state.exactSearch); wrapperDiv.appendChild(textDiv); if (imageUrl) { const imageElement = createImageElement(wrapperDiv, imageUrl, vocab, state.exactSearch); if (imageElement) { imageElement.addEventListener('click', () => { playAudio(soundUrl); }); } if (sentence) { appendSentenceAndTranslation(wrapperDiv, sentence, example.translation); } else { appendNoneText(wrapperDiv); } } const navigationDiv = createNavigationDiv(); const leftArrow = createLeftArrow(vocab, shouldAutoPlaySound); const rightArrow = createRightArrow(vocab, shouldAutoPlaySound); const containerDiv = createContainerDiv(leftArrow, wrapperDiv, rightArrow, navigationDiv); appendContainer(containerDiv); } if (CONFIG.AUTO_PLAY_SOUND && shouldAutoPlaySound) { playAudio(soundUrl); } } function removeExistingContainer() { const existingContainer = document.getElementById('immersion-kit-container'); if (existingContainer) { existingContainer.remove(); } } function shouldRenderContainer() { const resultVocabularySection = document.querySelector('.result.vocabulary'); const hboxWrapSection = document.querySelector('.hbox.wrap'); const subsectionMeanings = document.querySelector('.subsection-meanings'); const subsectionLabels = document.querySelectorAll('h6.subsection-label'); return resultVocabularySection || hboxWrapSection || subsectionMeanings || subsectionLabels.length >= 3; } function createWrapperDiv() { const wrapperDiv = document.createElement('div'); wrapperDiv.id = 'image-wrapper'; wrapperDiv.style.textAlign = 'center'; wrapperDiv.style.padding = '5px 0'; return wrapperDiv; } function createImageElement(wrapperDiv, imageUrl, vocab, exactSearch) { const searchVocab = exactSearch ? `「${vocab}」` : vocab; const titleText = `${searchVocab} #${state.currentExampleIndex + 1}`; return GM_addElement(wrapperDiv, 'img', { src: imageUrl, alt: 'Embedded Image', title: titleText, style: `max-width: ${CONFIG.IMAGE_WIDTH}; margin-top: 10px; cursor: pointer;` }); } 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 appendSentenceAndTranslation(wrapperDiv, sentence, translation) { const sentenceText = document.createElement('div'); sentenceText.innerHTML = highlightVocab(sentence, state.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 && translation) { const translationText = document.createElement('div'); translationText.innerHTML = replaceSpecialCharacters(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); } } function appendNoneText(wrapperDiv) { 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); } function createNavigationDiv() { 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'; return navigationDiv; } function createLeftArrow(vocab, shouldAutoPlaySound) { 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(); } }); return leftArrow; } function createRightArrow(vocab, shouldAutoPlaySound) { 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(); } }); return rightArrow; } function createContainerDiv(leftArrow, wrapperDiv, rightArrow, navigationDiv) { const containerDiv = document.createElement('div'); containerDiv.id = 'immersion-kit-container'; containerDiv.style.display = 'flex'; containerDiv.style.alignItems = 'center'; containerDiv.style.justifyContent = 'center'; // Center the content containerDiv.style.flexDirection = 'column'; // Stack elements vertically const arrowWrapperDiv = document.createElement('div'); arrowWrapperDiv.style.display = 'flex'; arrowWrapperDiv.style.alignItems = 'center'; arrowWrapperDiv.style.justifyContent = 'center'; // Center the arrows and image horizontally arrowWrapperDiv.append(leftArrow, wrapperDiv, rightArrow); containerDiv.append(arrowWrapperDiv, navigationDiv); return containerDiv; } function appendContainer(containerDiv) { 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'); 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); 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) { const existingDynamicDiv = vboxGap.querySelector('#dynamic-content'); if (existingDynamicDiv) { existingDynamicDiv.remove(); } const dynamicDiv = document.createElement('div'); dynamicDiv.id = 'dynamic-content'; dynamicDiv.appendChild(wrapper); if (window.location.href.includes('vocabulary')) { vboxGap.insertBefore(dynamicDiv, vboxGap.children[1]); } else { vboxGap.insertBefore(dynamicDiv, vboxGap.firstChild); } } } 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]); } } } function embedImageAndPlayAudio() { const existingNavigationDiv = document.getElementById('immersion-kit-embed'); if (existingNavigationDiv) existingNavigationDiv.remove(); const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/; 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;' }); const startIndex = Math.max(0, state.currentExampleIndex - CONFIG.NUMBER_OF_PRELOADS); const endIndex = Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUMBER_OF_PRELOADS); for (let i = startIndex; i <= endIndex; i++) { if (!state.preloadedIndices.has(i) && state.examples[i].image_url) { GM_addElement(preloadDiv, 'img', { src: state.examples[i].image_url }); state.preloadedIndices.add(i); } } } //MENU FUNCTIONS===================================================================================================================== function handleImportButtonClick() { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'application/json'; fileInput.addEventListener('change', importFavorites); fileInput.click(); } function exportFavorites() { const favorites = {}; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (!key.startsWith('CONFIG')) { 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!'); location.reload(); } catch (error) { alert('Error importing favorites:', error); } }; reader.readAsText(file); } function createConfirmationPopup(messageText, onYes, onNo) { const popupOverlay = document.createElement('div'); popupOverlay.style.position = 'fixed'; popupOverlay.style.top = '0'; popupOverlay.style.left = '0'; popupOverlay.style.width = '100%'; popupOverlay.style.height = '100%'; popupOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)'; popupOverlay.style.zIndex = '1001'; popupOverlay.style.display = 'flex'; popupOverlay.style.justifyContent = 'center'; popupOverlay.style.alignItems = 'center'; const popupContent = document.createElement('div'); popupContent.style.backgroundColor = 'var(--background-color)'; popupContent.style.padding = '20px'; popupContent.style.borderRadius = '5px'; popupContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'; popupContent.style.textAlign = 'center'; const message = document.createElement('p'); message.textContent = messageText; const yesButton = document.createElement('button'); yesButton.textContent = 'Yes'; yesButton.style.backgroundColor = '#C82800'; yesButton.style.marginRight = '10px'; yesButton.addEventListener('click', () => { onYes(); document.body.removeChild(popupOverlay); }); const noButton = document.createElement('button'); noButton.textContent = 'No'; noButton.addEventListener('click', () => { onNo(); document.body.removeChild(popupOverlay); }); popupContent.appendChild(message); popupContent.appendChild(yesButton); popupContent.appendChild(noButton); popupOverlay.appendChild(popupContent); document.body.appendChild(popupOverlay); } function saveConfig() { const overlay = document.getElementById('overlayMenu'); if (!overlay) return; const inputs = overlay.querySelectorAll('input, span'); let minimumExampleLengthChanged = false; let newMinimumExampleLength; const changes = {}; inputs.forEach(input => { const key = input.getAttribute('data-key'); const type = input.getAttribute('data-type'); let value; if (type === 'boolean') { value = input.checked; } else if (type === 'number') { value = parseFloat(input.textContent); } else if (type === 'string') { value = input.textContent; } if (key && type) { const typePart = input.getAttribute('data-type-part'); const originalFormattedType = typePart.slice(1, -1); if (key === 'MINIMUM_EXAMPLE_LENGTH' && CONFIG.MINIMUM_EXAMPLE_LENGTH !== value) { minimumExampleLengthChanged = true; newMinimumExampleLength = value; } changes[`CONFIG.${key}`] = `${value}${originalFormattedType}`; } }); if (minimumExampleLengthChanged) { createConfirmationPopup( 'Changing Minimum Example Length will break your current favorites. Are you sure? Deleting favorites is recommended.', () => { CONFIG.MINIMUM_EXAMPLE_LENGTH = newMinimumExampleLength; localStorage.setItem('CONFIG.MINIMUM_EXAMPLE_LENGTH', newMinimumExampleLength); applyChanges(changes); finalizeSaveConfig(); location.reload(); }, () => { document.body.removeChild(overlay); document.body.appendChild(createOverlayMenu()); } ); } else { applyChanges(changes); finalizeSaveConfig(); setPageWidth(); } } function applyChanges(changes) { for (const key in changes) { localStorage.setItem(key, changes[key]); } } function finalizeSaveConfig() { loadConfig(); renderImageAndPlayAudio(state.vocab, CONFIG.AUTO_PLAY_SOUND); const overlay = document.getElementById('overlayMenu'); if (overlay) { document.body.removeChild(overlay); } } function loadConfig() { for (const key in CONFIG) { const valueType = typeof CONFIG[key]; const savedValue = localStorage.getItem(`CONFIG.${key}`); if (savedValue !== null) { if (valueType === 'boolean') { CONFIG[key] = savedValue === 'true'; } else if (valueType === 'number') { CONFIG[key] = parseFloat(savedValue); } else if (valueType === 'string') { CONFIG[key] = savedValue; } } } } function createButton(text, margin, onClick, width) { const button = document.createElement('button'); button.textContent = text; button.style.margin = margin; button.style.width = width; button.style.textAlign = 'center'; button.style.display = 'inline-block'; button.style.lineHeight = '30px'; button.style.padding = '5px 0'; button.addEventListener('click', onClick); return button; } function createMenuButtons() { const exportImportButtonWidth = '200px'; const actionButtonWidth = '100px'; const exportButton = createButton('Export Favorites', '10px', exportFavorites, exportImportButtonWidth); const importButton = createButton('Import Favorites', '10px', handleImportButtonClick, exportImportButtonWidth); const exportImportContainer = document.createElement('div'); exportImportContainer.style.textAlign = 'center'; exportImportContainer.style.marginTop = '10px'; exportImportContainer.append(exportButton, importButton); const closeButton = createButton('Close', '10px', () => { loadConfig(); document.body.removeChild(document.getElementById('overlayMenu')); }, actionButtonWidth); const saveButton = createButton('Save', '10px', saveConfig, actionButtonWidth); const defaultButton = createButton('Default', '10px', () => { createConfirmationPopup( 'This will reset all your settings to default. Are you sure?', () => { Object.keys(localStorage).forEach(key => { if (key.startsWith('CONFIG')) { localStorage.removeItem(key); } }); location.reload(); }, () => { const overlay = document.getElementById('overlayMenu'); if (overlay) { document.body.removeChild(overlay); } loadConfig(); document.body.appendChild(createOverlayMenu()); } ); }, actionButtonWidth); defaultButton.style.backgroundColor = '#C82800'; defaultButton.style.color = 'white'; const deleteButton = createButton('DELETE', '10px', () => { createConfirmationPopup( 'This will delete all your favorites. Are you sure?', () => { Object.keys(localStorage).forEach(key => { if (!key.startsWith('CONFIG')) { localStorage.removeItem(key); } }); location.reload(); }, () => { const overlay = document.getElementById('overlayMenu'); if (overlay) { document.body.removeChild(overlay); } loadConfig(); document.body.appendChild(createOverlayMenu()); } ); }, actionButtonWidth); deleteButton.style.backgroundColor = '#C82800'; deleteButton.style.color = 'white'; const actionButtonsContainer = document.createElement('div'); actionButtonsContainer.style.textAlign = 'center'; actionButtonsContainer.style.marginTop = '10px'; actionButtonsContainer.append(closeButton, saveButton, defaultButton, deleteButton); const buttonContainer = document.createElement('div'); buttonContainer.append(exportImportContainer, actionButtonsContainer); return buttonContainer; } function createOverlayMenu() { const overlay = document.createElement('div'); overlay.id = 'overlayMenu'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)'; overlay.style.zIndex = '1000'; overlay.style.display = 'flex'; overlay.style.justifyContent = 'center'; overlay.style.alignItems = 'center'; const menuContent = document.createElement('div'); menuContent.style.backgroundColor = 'var(--background-color)'; menuContent.style.color = 'var(--text-color)'; menuContent.style.padding = '20px'; menuContent.style.borderRadius = '5px'; menuContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'; menuContent.style.width = '80%'; menuContent.style.maxWidth = '550px'; menuContent.style.maxHeight = '80%'; menuContent.style.overflowY = 'auto'; for (const [key, value] of Object.entries(CONFIG)) { const optionContainer = document.createElement('div'); optionContainer.style.marginBottom = '10px'; optionContainer.style.display = 'flex'; optionContainer.style.alignItems = 'center'; const leftContainer = document.createElement('div'); leftContainer.style.flex = '1'; leftContainer.style.display = 'flex'; leftContainer.style.alignItems = 'center'; const rightContainer = document.createElement('div'); rightContainer.style.flex = '1'; rightContainer.style.display = 'flex'; rightContainer.style.alignItems = 'center'; rightContainer.style.justifyContent = 'center'; const label = document.createElement('label'); label.textContent = key.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' '); label.style.marginRight = '10px'; leftContainer.appendChild(label); if (typeof value === 'boolean') { const checkboxContainer = document.createElement('div'); checkboxContainer.style.display = 'flex'; checkboxContainer.style.alignItems = 'center'; checkboxContainer.style.justifyContent = 'center'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = value; checkbox.setAttribute('data-key', key); checkbox.setAttribute('data-type', 'boolean'); checkbox.setAttribute('data-type-part', ''); checkboxContainer.appendChild(checkbox); rightContainer.appendChild(checkboxContainer); } else if (typeof value === 'number') { const numberContainer = document.createElement('div'); numberContainer.style.display = 'flex'; numberContainer.style.alignItems = 'center'; numberContainer.style.justifyContent = 'center'; const decrementButton = document.createElement('button'); decrementButton.textContent = '-'; decrementButton.style.marginRight = '5px'; const input = document.createElement('span'); input.textContent = value; input.style.margin = '0 10px'; input.style.minWidth = '3ch'; input.style.textAlign = 'center'; input.setAttribute('data-key', key); input.setAttribute('data-type', 'number'); input.setAttribute('data-type-part', ''); const incrementButton = document.createElement('button'); incrementButton.textContent = '+'; incrementButton.style.marginLeft = '5px'; const updateButtonStates = () => { let currentValue = parseFloat(input.textContent); if (currentValue <= 0) { decrementButton.disabled = true; decrementButton.style.color = 'grey'; } else { decrementButton.disabled = false; decrementButton.style.color = ''; } if (key === 'SOUND_VOLUME' && currentValue >= 100) { incrementButton.disabled = true; incrementButton.style.color = 'grey'; } else { incrementButton.disabled = false; incrementButton.style.color = ''; } }; decrementButton.addEventListener('click', () => { let currentValue = parseFloat(input.textContent); if (currentValue > 0) { if (currentValue > 200) { input.textContent = currentValue - 25; } else if (currentValue > 20) { input.textContent = currentValue - 5; } else { input.textContent = currentValue - 1; } updateButtonStates(); } }); incrementButton.addEventListener('click', () => { let currentValue = parseFloat(input.textContent); if (key === 'SOUND_VOLUME' && currentValue >= 100) { return; } if (currentValue >= 200) { input.textContent = currentValue + 25; } else if (currentValue >= 20) { input.textContent = currentValue + 5; } else { input.textContent = currentValue + 1; } updateButtonStates(); }); numberContainer.appendChild(decrementButton); numberContainer.appendChild(input); numberContainer.appendChild(incrementButton); rightContainer.appendChild(numberContainer); // Initialize button states updateButtonStates(); } else if (typeof value === 'string') { const typeParts = value.split(/(\d+)/).filter(Boolean); const numberParts = typeParts.filter(part => !isNaN(part)).map(Number); const numberContainer = document.createElement('div'); numberContainer.style.display = 'flex'; numberContainer.style.alignItems = 'center'; numberContainer.style.justifyContent = 'center'; const typeSpan = document.createElement('span'); const formattedType = '(' + typeParts.filter(part => isNaN(part)).join('').replace(/_/g, ' ').toLowerCase() + ')'; typeSpan.textContent = formattedType; typeSpan.style.marginRight = '10px'; leftContainer.appendChild(typeSpan); typeParts.forEach(part => { if (!isNaN(part)) { const decrementButton = document.createElement('button'); decrementButton.textContent = '-'; decrementButton.style.marginRight = '5px'; const input = document.createElement('span'); input.textContent = part; input.style.margin = '0 10px'; input.style.minWidth = '3ch'; input.style.textAlign = 'center'; input.setAttribute('data-key', key); input.setAttribute('data-type', 'string'); input.setAttribute('data-type-part', formattedType); const incrementButton = document.createElement('button'); incrementButton.textContent = '+'; incrementButton.style.marginLeft = '5px'; const updateButtonStates = () => { let currentValue = parseFloat(input.textContent); if (currentValue <= 0) { decrementButton.disabled = true; decrementButton.style.color = 'grey'; } else { decrementButton.disabled = false; decrementButton.style.color = ''; } if (key === 'SOUND_VOLUME' && currentValue >= 100) { incrementButton.disabled = true; incrementButton.style.color = 'grey'; } else { incrementButton.disabled = false; incrementButton.style.color = ''; } }; decrementButton.addEventListener('click', () => { let currentValue = parseFloat(input.textContent); if (currentValue > 0) { if (currentValue > 200) { input.textContent = currentValue - 25; } else if (currentValue > 20) { input.textContent = currentValue - 5; } else { input.textContent = currentValue - 1; } updateButtonStates(); } }); incrementButton.addEventListener('click', () => { let currentValue = parseFloat(input.textContent); if (key === 'SOUND_VOLUME' && currentValue >= 100) { return; } if (currentValue >= 200) { input.textContent = currentValue + 25; } else if (currentValue >= 20) { input.textContent = currentValue + 5; } else { input.textContent = currentValue + 1; } updateButtonStates(); }); numberContainer.appendChild(decrementButton); numberContainer.appendChild(input); numberContainer.appendChild(incrementButton); // Initialize button states updateButtonStates(); } }); rightContainer.appendChild(numberContainer); } optionContainer.appendChild(leftContainer); optionContainer.appendChild(rightContainer); menuContent.appendChild(optionContainer); } const menuButtons = createMenuButtons(); menuContent.appendChild(menuButtons); overlay.appendChild(menuContent); return overlay; } //MAIN FUNCTIONS===================================================================================================================== function onPageLoad() { state.embedAboveSubsectionMeanings = false; const url = window.location.href; if (url.includes('/vocabulary/')) { state.vocab = parseVocabFromVocabulary(); } else if (url.includes('c=')) { state.vocab = parseVocabFromAnswer(); } else if (url.includes('/kanji/')) { state.vocab = parseVocabFromKanji(); } else { state.vocab = parseVocabFromReview(); } const { index, exactState } = getStoredData(state.vocab); state.currentExampleIndex = index; state.exactSearch = exactState; if (state.vocab && !state.apiDataFetched) { getImmersionKitData(state.vocab, state.exactSearch) .then(() => { preloadImages(); if (!/https:\/\/jpdb\.io\/review(#a)?$/.test(url)) { embedImageAndPlayAudio(); } }) .catch(console.error); } else if (state.apiDataFetched) { embedImageAndPlayAudio(); preloadImages(); setPageWidth(); } } function setPageWidth() { document.body.style.maxWidth = CONFIG.PAGE_WIDTH; } const observer = new MutationObserver(() => { if (window.location.href !== observer.lastUrl) { observer.lastUrl = window.location.href; onPageLoad(); } }); observer.lastUrl = window.location.href; observer.observe(document, { subtree: true, childList: true }); window.addEventListener('load', onPageLoad); window.addEventListener('popstate', onPageLoad); window.addEventListener('hashchange', onPageLoad); loadConfig(); setPageWidth(); preloadImages(); })();