Advertisement
Guest User

Untitled

a guest
Aug 28th, 2024
228
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 18.32 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name         JPDB Immersion Kit Examples
  3. // @version      1.0
  4. // @description  Embeds anime images & audio examples into JPDB review and vocabulary pages using Immersion Kit's API. Compatible only with TamperMonkey.
  5. // @match        https://jpdb.io/review*
  6. // @match        https://jpdb.io/vocabulary/*
  7. // @match        https://jpdb.io/kanji/*
  8. // @grant        GM_addElement
  9. // @grant        GM_xmlhttpRequest
  10. // ==/UserScript==
  11.  
  12. (function() {
  13.     'use strict';
  14.  
  15.     const CONFIG = {
  16.         IMAGE_WIDTH: '400px',
  17.         ENABLE_EXAMPLE_TRANSLATION: true,
  18.         SENTENCE_FONT_SIZE: '120%',
  19.         TRANSLATION_FONT_SIZE: '85%',
  20.         COLORED_SENTENCE_TEXT: true,
  21.         AUTO_PLAY_SOUND: true,
  22.         SOUND_VOLUME: 0.5,
  23.         NUM_PRELOADS: 1
  24.     };
  25.  
  26.     const state = {
  27.         currentExampleIndex: 0,
  28.         examples: [],
  29.         apiDataFetched: false,
  30.         vocab: '',
  31.         embedAboveSubsectionMeanings: false,
  32.         preloadedIndices: new Set()
  33.     };
  34.  
  35.     function getImmersionKitData(vocab, callback) {
  36.         const url = `https://api.immersionkit.com/look_up_dictionary?keyword=${encodeURIComponent(vocab)}&sort=shortness`;
  37.         GM_xmlhttpRequest({
  38.             method: "GET",
  39.             url: url,
  40.             onload: function(response) {
  41.                 if (response.status === 200) {
  42.                     try {
  43.                         const jsonData = JSON.parse(response.responseText);
  44.                         if (jsonData.data && jsonData.data[0] && jsonData.data[0].examples) {
  45.                             state.examples = jsonData.data[0].examples;
  46.                             state.apiDataFetched = true;
  47.                             state.currentExampleIndex = parseInt(getStoredData(vocab)) || 0;
  48.                         }
  49.                     } catch (e) {
  50.                         console.error('Error parsing JSON response:', e);
  51.                     }
  52.                 }
  53.                 callback();
  54.             },
  55.             onerror: function(error) {
  56.                 console.error('Error fetching data:', error);
  57.                 callback();
  58.             }
  59.         });
  60.     }
  61.  
  62.     function getStoredData(key) {
  63.         return localStorage.getItem(key);
  64.     }
  65.  
  66.     function storeData(key, value) {
  67.         localStorage.setItem(key, value);
  68.     }
  69.  
  70.     function exportFavorites() {
  71.         const favorites = {};
  72.         for (let i = 0; i < localStorage.length; i++) {
  73.             const key = localStorage.key(i);
  74.             favorites[key] = localStorage.getItem(key);
  75.         }
  76.         const blob = new Blob([JSON.stringify(favorites, null, 2)], { type: 'application/json' });
  77.         const url = URL.createObjectURL(blob);
  78.         const a = document.createElement('a');
  79.         a.href = url;
  80.         a.download = 'favorites.json';
  81.         document.body.appendChild(a);
  82.         a.click();
  83.         document.body.removeChild(a);
  84.         URL.revokeObjectURL(url);
  85.     }
  86.  
  87.     function importFavorites(event) {
  88.         const file = event.target.files[0];
  89.         if (!file) return;
  90.  
  91.         const reader = new FileReader();
  92.         reader.onload = function(e) {
  93.             try {
  94.                 const favorites = JSON.parse(e.target.result);
  95.                 for (const key in favorites) {
  96.                     localStorage.setItem(key, favorites[key]);
  97.                 }
  98.                 alert('Favorites imported successfully!');
  99.             } catch (error) {
  100.                 alert('Error importing favorites:', error);
  101.             }
  102.         };
  103.         reader.readAsText(file);
  104.     }
  105.  
  106.     function parseVocabFromAnswer() {
  107.         const elements = document.querySelectorAll('a[href*="/kanji/"], a[href*="/vocabulary/"]');
  108.         for (const element of elements) {
  109.             const href = element.getAttribute('href');
  110.             const text = element.textContent.trim();
  111.             const match = href.match(/\/(kanji|vocabulary)\/(?:\d+\/)?([^\#]*)#/);
  112.             if (match) return match[2].trim();
  113.             if (text) return text.trim();
  114.         }
  115.         return '';
  116.     }
  117.  
  118.     function parseVocabFromReview() {
  119.         const kindElement = document.querySelector('.kind');
  120.         if (!kindElement) return '';
  121.  
  122.         const kindText = kindElement.textContent.trim();
  123.         if (kindText !== 'Kanji' && kindText !== 'Vocabulary') return '';
  124.  
  125.         if (kindText === 'Vocabulary') {
  126.             const plainElement = document.querySelector('.plain');
  127.             if (!plainElement) return '';
  128.  
  129.             let vocabulary = plainElement.textContent.trim();
  130.             const nestedVocabularyElement = plainElement.querySelector('div:not([style])');
  131.             if (nestedVocabularyElement) {
  132.                 vocabulary = nestedVocabularyElement.textContent.trim();
  133.             }
  134.             const specificVocabularyElement = plainElement.querySelector('div:nth-child(3)');
  135.             if (specificVocabularyElement) {
  136.                 vocabulary = specificVocabularyElement.textContent.trim();
  137.             }
  138.  
  139.             const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
  140.             if (kanjiRegex.test(vocabulary) || vocabulary) {
  141.                 return vocabulary;
  142.             }
  143.         } else if (kindText === 'Kanji') {
  144.             const hiddenInput = document.querySelector('input[name="c"]');
  145.             if (!hiddenInput) return '';
  146.  
  147.             const vocab = hiddenInput.value.split(',')[1];
  148.             const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
  149.             if (kanjiRegex.test(vocab)) {
  150.                 return vocab;
  151.             }
  152.         }
  153.  
  154.         return '';
  155.     }
  156.  
  157.     function parseVocabFromVocabulary() {
  158.         const url = window.location.href;
  159.         const match = url.match(/https:\/\/jpdb\.io\/vocabulary\/(\d+)\/([^\#]*)#a/);
  160.         if (match) {
  161.             let vocab = match[2];
  162.             state.embedAboveSubsectionMeanings = true;
  163.             vocab = vocab.split('/')[0];
  164.             return decodeURIComponent(vocab);
  165.         }
  166.         return '';
  167.     }
  168.  
  169.     function parseVocabFromKanji() {
  170.         const url = window.location.href;
  171.         const match = url.match(/https:\/\/jpdb\.io\/kanji\/([^#]*)#a/);
  172.         if (match) {
  173.             return decodeURIComponent(match[1]);
  174.         }
  175.         return '';
  176.     }
  177.  
  178.     function highlightVocab(sentence, vocab) {
  179.         if (!CONFIG.COLORED_SENTENCE_TEXT) return sentence;
  180.         return vocab.split('').reduce((acc, char) => {
  181.             const regex = new RegExp(char, 'g');
  182.             return acc.replace(regex, `<span style="color: var(--outline-input-color);">${char}</span>`);
  183.         }, sentence);
  184.     }
  185.  
  186.     function createIconLink(iconClass, onClick, marginLeft = '0') {
  187.         const link = document.createElement('a');
  188.         link.href = '#';
  189.         link.style.border = '0';
  190.         link.style.display = 'inline-flex';
  191.         link.style.verticalAlign = 'middle';
  192.         link.style.marginLeft = marginLeft;
  193.  
  194.         const icon = document.createElement('i');
  195.         icon.className = iconClass;
  196.         icon.style.fontSize = '1.4rem';
  197.         icon.style.opacity = '1.0';
  198.         icon.style.verticalAlign = 'baseline';
  199.         icon.style.color = '#3d81ff';
  200.  
  201.         link.appendChild(icon);
  202.         link.addEventListener('click', onClick);
  203.         return link;
  204.     }
  205.  
  206.     function createStarLink() {
  207.         const link = document.createElement('a');
  208.         link.href = '#';
  209.         link.style.border = '0';
  210.         link.style.display = 'inline-flex';
  211.         link.style.verticalAlign = 'middle';
  212.  
  213.         const starIcon = document.createElement('span');
  214.         starIcon.textContent = state.currentExampleIndex === parseInt(getStoredData(state.vocab)) ? '★' : '☆';
  215.         starIcon.style.fontSize = '1.4rem';
  216.         starIcon.style.marginLeft = '0.5rem';
  217.         starIcon.style.color = '3D8DFF';
  218.         starIcon.style.verticalAlign = 'middle';
  219.         starIcon.style.position = 'relative';
  220.         starIcon.style.top = '-2px';
  221.  
  222.         link.appendChild(starIcon);
  223.         link.addEventListener('click', (event) => {
  224.             event.preventDefault();
  225.             const favoriteIndex = parseInt(getStoredData(state.vocab));
  226.             storeData(state.vocab, favoriteIndex === state.currentExampleIndex ? null : state.currentExampleIndex);
  227.             embedImageAndPlayAudio();
  228.         });
  229.         return link;
  230.     }
  231.  
  232.     function createExportImportButtons() {
  233.         const exportButton = document.createElement('button');
  234.         exportButton.textContent = 'Export Favorites';
  235.         exportButton.style.marginRight = '10px';
  236.         exportButton.addEventListener('click', exportFavorites);
  237.  
  238.         const importButton = document.createElement('button');
  239.         importButton.textContent = 'Import Favorites';
  240.         importButton.addEventListener('click', () => {
  241.             const fileInput = document.createElement('input');
  242.             fileInput.type = 'file';
  243.             fileInput.accept = 'application/json';
  244.             fileInput.addEventListener('change', importFavorites);
  245.             fileInput.click();
  246.         });
  247.  
  248.         const buttonContainer = document.createElement('div');
  249.         buttonContainer.style.textAlign = 'center';
  250.         buttonContainer.style.marginTop = '10px';
  251.         buttonContainer.append(exportButton, importButton);
  252.  
  253.         document.body.appendChild(buttonContainer);
  254.     }
  255.  
  256.     function playAudio(soundUrl) {
  257.         if (soundUrl) {
  258.             const audioElement = GM_addElement('audio', { src: soundUrl, autoplay: true });
  259.             audioElement.volume = CONFIG.SOUND_VOLUME;
  260.         }
  261.     }
  262.  
  263.     function renderImageAndPlayAudio(vocab, shouldAutoPlaySound) {
  264.         const example = state.examples[state.currentExampleIndex] || {};
  265.         const imageUrl = example.image_url || null;
  266.         const soundUrl = example.sound_url || null;
  267.         const sentence = example.sentence || null;
  268.  
  269.         const resultVocabularySection = document.querySelector('.result.vocabulary');
  270.         const hboxWrapSection = document.querySelector('.hbox.wrap');
  271.         const subsectionMeanings = document.querySelector('.subsection-meanings');
  272.         const subsectionLabels = document.querySelectorAll('h6.subsection-label');
  273.  
  274.         // Remove any existing embed before creating a new one
  275.         const existingEmbed = document.getElementById('immersion-kit-embed');
  276.         if (existingEmbed) {
  277.             existingEmbed.remove();
  278.         }
  279.  
  280.         if (resultVocabularySection || hboxWrapSection || subsectionMeanings || subsectionLabels.length >= 3) {
  281.             const wrapperDiv = document.createElement('div');
  282.             wrapperDiv.id = 'image-wrapper';
  283.             wrapperDiv.style.textAlign = 'center';
  284.             wrapperDiv.style.padding = '5px 0';
  285.  
  286.             const textDiv = document.createElement('div');
  287.             textDiv.style.marginBottom = '5px';
  288.             textDiv.style.lineHeight = '1.4rem';
  289.  
  290.             const contentText = document.createElement('span');
  291.             contentText.textContent = 'Immersion Kit';
  292.             contentText.style.color = 'var(--subsection-label-color)';
  293.             contentText.style.fontSize = '85%';
  294.             contentText.style.marginRight = '0.5rem';
  295.             contentText.style.verticalAlign = 'middle';
  296.  
  297.             const speakerLink = createIconLink('ti ti-volume', (event) => {
  298.                 event.preventDefault();
  299.                 playAudio(soundUrl);
  300.             }, '0.5rem');
  301.  
  302.             const starLink = createStarLink();
  303.  
  304.             textDiv.append(contentText, speakerLink, starLink);
  305.             wrapperDiv.appendChild(textDiv);
  306.  
  307.             if (imageUrl) {
  308.                 const imageElement = GM_addElement(wrapperDiv, 'img', {
  309.                     src: imageUrl,
  310.                     alt: 'Embedded Image',
  311.                     style: `max-width: ${CONFIG.IMAGE_WIDTH}; margin-top: 10px; cursor: pointer;`
  312.                 });
  313.  
  314.                 if (imageElement) {
  315.                     imageElement.addEventListener('click', () => {
  316.                         speakerLink.click();
  317.                     });
  318.                 }
  319.  
  320.                 if (sentence) {
  321.                     const sentenceText = document.createElement('div');
  322.                     sentenceText.innerHTML = highlightVocab(sentence, vocab);
  323.                     sentenceText.style.marginTop = '10px';
  324.                     sentenceText.style.fontSize = CONFIG.SENTENCE_FONT_SIZE;
  325.                     sentenceText.style.color = 'lightgray';
  326.                     wrapperDiv.appendChild(sentenceText);
  327.  
  328.                     if (CONFIG.ENABLE_EXAMPLE_TRANSLATION && example.translation) {
  329.                         const translationText = document.createElement('div');
  330.                         translationText.innerHTML = replaceSpecialCharacters(example.translation);
  331.                         translationText.style.marginTop = '5px';
  332.                         translationText.style.fontSize = CONFIG.TRANSLATION_FONT_SIZE;
  333.                         translationText.style.color = 'var(--subsection-label-color)';
  334.                         wrapperDiv.appendChild(translationText);
  335.                     }
  336.                 } else {
  337.                     const noneText = document.createElement('div');
  338.                     noneText.textContent = 'None';
  339.                     noneText.style.marginTop = '10px';
  340.                     noneText.style.fontSize = '85%';
  341.                     noneText.style.color = 'var(--subsection-label-color)';
  342.                     wrapperDiv.appendChild(noneText);
  343.                 }
  344.             }
  345.  
  346.             // Create a fixed-width container for arrows and image
  347.             const navigationDiv = document.createElement('div');
  348.             navigationDiv.id = 'immersion-kit-embed';
  349.             navigationDiv.style.display = 'flex';
  350.             navigationDiv.style.justifyContent = 'center';
  351.             navigationDiv.style.alignItems = 'center';
  352.             navigationDiv.style.maxWidth = CONFIG.IMAGE_WIDTH;
  353.             navigationDiv.style.margin = '0 auto';
  354.  
  355.             const leftArrow = document.createElement('button');
  356.             leftArrow.textContent = '<';
  357.             leftArrow.style.marginRight = '10px';
  358.             leftArrow.disabled = state.currentExampleIndex === 0;
  359.             leftArrow.addEventListener('click', () => {
  360.                 if (state.currentExampleIndex > 0) {
  361.                     state.currentExampleIndex--;
  362.                     embedImageAndPlayAudio();
  363.                     preloadImages();
  364.                 }
  365.             });
  366.  
  367.             const rightArrow = document.createElement('button');
  368.             rightArrow.textContent = '>';
  369.             rightArrow.style.marginLeft = '10px';
  370.             rightArrow.disabled = state.currentExampleIndex >= state.examples.length - 1;
  371.             rightArrow.addEventListener('click', () => {
  372.                 if (state.currentExampleIndex < state.examples.length - 1) {
  373.                     state.currentExampleIndex++;
  374.                     embedImageAndPlayAudio();
  375.                     preloadImages();
  376.                 }
  377.             });
  378.  
  379.             navigationDiv.append(leftArrow, wrapperDiv, rightArrow);
  380.  
  381.             if (state.embedAboveSubsectionMeanings && subsectionMeanings) {
  382.                 subsectionMeanings.parentNode.insertBefore(navigationDiv, subsectionMeanings);
  383.             } else if (resultVocabularySection) {
  384.                 resultVocabularySection.parentNode.insertBefore(navigationDiv, resultVocabularySection);
  385.             } else if (hboxWrapSection) {
  386.                 hboxWrapSection.parentNode.insertBefore(navigationDiv, hboxWrapSection);
  387.             } else if (subsectionLabels.length >= 4) {
  388.                 subsectionLabels[3].parentNode.insertBefore(navigationDiv, subsectionLabels[3]);
  389.             }
  390.         }
  391.  
  392.         if (CONFIG.AUTO_PLAY_SOUND && shouldAutoPlaySound) {
  393.             playAudio(soundUrl);
  394.         }
  395.     }
  396.  
  397.     function embedImageAndPlayAudio() {
  398.         const existingNavigationDiv = document.getElementById('immersion-kit-embed');
  399.         if (existingNavigationDiv) existingNavigationDiv.remove();
  400.  
  401.         const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
  402.  
  403.         if (state.vocab && !state.apiDataFetched) {
  404.             getImmersionKitData(state.vocab, () => {
  405.                 renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
  406.                 preloadImages();
  407.             });
  408.         } else {
  409.             renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
  410.             preloadImages();
  411.         }
  412.     }
  413.  
  414.     function replaceSpecialCharacters(text) {
  415.         return text.replace(/<br>/g, '\n').replace(/&quot;/g, '"').replace(/\n/g, '<br>');
  416.     }
  417.  
  418.     function preloadImages() {
  419.         const preloadDiv = GM_addElement(document.body, 'div', { style: 'display: none;' });
  420.  
  421.         for (let i = Math.max(0, state.currentExampleIndex - CONFIG.NUM_PRELOADS); i <= Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUM_PRELOADS); i++) {
  422.             if (!state.preloadedIndices.has(i)) {
  423.                 const example = state.examples[i];
  424.                 if (example.image_url) {
  425.                     GM_addElement(preloadDiv, 'img', { src: example.image_url });
  426.                     state.preloadedIndices.add(i);
  427.                 }
  428.             }
  429.         }
  430.     }
  431.  
  432.     function onUrlChange() {
  433.         state.embedAboveSubsectionMeanings = false;
  434.         if (window.location.href.includes('/vocabulary/')) {
  435.             state.vocab = parseVocabFromVocabulary();
  436.         } else if (window.location.href.includes('c=')) {
  437.             state.vocab = parseVocabFromAnswer();
  438.         } else if (window.location.href.includes('/kanji/')) {
  439.             state.vocab = parseVocabFromKanji();
  440.         } else {
  441.             state.vocab = parseVocabFromReview();
  442.         }
  443.  
  444.         const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
  445.         const shouldAutoPlaySound = !reviewUrlPattern.test(window.location.href);
  446.  
  447.         if (state.vocab) {
  448.             embedImageAndPlayAudio();
  449.         }
  450.     }
  451.  
  452.     const observer = new MutationObserver(() => {
  453.         if (window.location.href !== observer.lastUrl) {
  454.             observer.lastUrl = window.location.href;
  455.             onUrlChange();
  456.         }
  457.     });
  458.  
  459.     observer.lastUrl = window.location.href;
  460.     observer.observe(document, { subtree: true, childList: true });
  461.  
  462.     onUrlChange();
  463.     createExportImportButtons();
  464. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement