JPDB Immersion Kit Examples

Embeds anime images & audio examples into JPDB review and vocabulary pages using Immersion Kit's API. Compatible only with TamperMonkey.

目前为 2024-09-08 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name JPDB Immersion Kit Examples
  3. // @version 1.5
  4. // @description Embeds anime images & audio examples into JPDB review and vocabulary pages using Immersion Kit's API. Compatible only with TamperMonkey.
  5. // @author awoo
  6. // @namespace jpdb-immersion-kit-examples
  7. // @match https://jpdb.io/review*
  8. // @match https://jpdb.io/vocabulary/*
  9. // @match https://jpdb.io/kanji/*
  10. // @grant GM_addElement
  11. // @grant GM_xmlhttpRequest
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. const CONFIG = {
  19. IMAGE_WIDTH: '400px',
  20. ENABLE_EXAMPLE_TRANSLATION: true,
  21. SENTENCE_FONT_SIZE: '120%',
  22. TRANSLATION_FONT_SIZE: '85%',
  23. COLORED_SENTENCE_TEXT: true,
  24. AUTO_PLAY_SOUND: true,
  25. SOUND_VOLUME: 0.8,
  26. NUM_PRELOADS: 1,
  27. WIDE_MODE: true,
  28. PAGE_MAX_WIDTH: '75rem'
  29.  
  30. };
  31.  
  32. const state = {
  33. currentExampleIndex: 0,
  34. examples: [],
  35. apiDataFetched: false,
  36. vocab: '',
  37. embedAboveSubsectionMeanings: false,
  38. preloadedIndices: new Set(),
  39. currentAudio: null,
  40. exactSearch: true
  41. };
  42.  
  43. function getImmersionKitData(vocab, exactSearch, callback) {
  44. const searchVocab = exactSearch ? `「${vocab}」` : vocab;
  45. const url = `https://api.immersionkit.com/look_up_dictionary?keyword=${encodeURIComponent(searchVocab)}&sort=shortness`;
  46.  
  47. GM_xmlhttpRequest({
  48. method: "GET",
  49. url: url,
  50. onload: function(response) {
  51. if (response.status === 200) {
  52. try {
  53. const jsonData = JSON.parse(response.responseText);
  54. if (jsonData.data && jsonData.data[0] && jsonData.data[0].examples) {
  55. state.examples = jsonData.data[0].examples;
  56. state.apiDataFetched = true;
  57. }
  58. } catch (e) {
  59. console.error('Error parsing JSON response:', e);
  60. }
  61. }
  62. callback();
  63. },
  64. onerror: function(error) {
  65. console.error('Error fetching data:', error);
  66. callback();
  67. }
  68. });
  69. }
  70.  
  71. function getStoredData(key) {
  72. const storedValue = localStorage.getItem(key);
  73. if (storedValue) {
  74. const [index, exactState] = storedValue.split(',');
  75. return {
  76. index: parseInt(index, 10),
  77. exactState: exactState === '1'
  78. };
  79. }
  80. return { index: 0, exactState: state.exactSearch };
  81. }
  82.  
  83. function storeData(key, index, exactState) {
  84. const value = `${index},${exactState ? 1 : 0}`;
  85. localStorage.setItem(key, value);
  86. }
  87.  
  88. function exportFavorites() {
  89. const favorites = {};
  90. for (let i = 0; i < localStorage.length; i++) {
  91. const key = localStorage.key(i);
  92. favorites[key] = localStorage.getItem(key);
  93. }
  94. const blob = new Blob([JSON.stringify(favorites, null, 2)], { type: 'application/json' });
  95. const url = URL.createObjectURL(blob);
  96. const a = document.createElement('a');
  97. a.href = url;
  98. a.download = 'favorites.json';
  99. document.body.appendChild(a);
  100. a.click();
  101. document.body.removeChild(a);
  102. URL.revokeObjectURL(url);
  103. }
  104.  
  105. function importFavorites(event) {
  106. const file = event.target.files[0];
  107. if (!file) return;
  108.  
  109. const reader = new FileReader();
  110. reader.onload = function(e) {
  111. try {
  112. const favorites = JSON.parse(e.target.result);
  113. for (const key in favorites) {
  114. localStorage.setItem(key, favorites[key]);
  115. }
  116. alert('Favorites imported successfully!');
  117. } catch (error) {
  118. alert('Error importing favorites:', error);
  119. }
  120. };
  121. reader.readAsText(file);
  122. }
  123.  
  124. function parseVocabFromAnswer() {
  125. const elements = document.querySelectorAll('a[href*="/kanji/"], a[href*="/vocabulary/"]');
  126. for (const element of elements) {
  127. const href = element.getAttribute('href');
  128. const text = element.textContent.trim();
  129. const match = href.match(/\/(kanji|vocabulary)\/(?:\d+\/)?([^\#]*)#/);
  130. if (match) return match[2].trim();
  131. if (text) return text.trim();
  132. }
  133. return '';
  134. }
  135.  
  136. function parseVocabFromReview() {
  137. const kindElement = document.querySelector('.kind');
  138. if (!kindElement) return '';
  139.  
  140. const kindText = kindElement.textContent.trim();
  141. if (kindText !== 'Kanji' && kindText !== 'Vocabulary') return '';
  142.  
  143. if (kindText === 'Vocabulary') {
  144. const plainElement = document.querySelector('.plain');
  145. if (!plainElement) return '';
  146.  
  147. let vocabulary = plainElement.textContent.trim();
  148. const nestedVocabularyElement = plainElement.querySelector('div:not([style])');
  149. if (nestedVocabularyElement) {
  150. vocabulary = nestedVocabularyElement.textContent.trim();
  151. }
  152. const specificVocabularyElement = plainElement.querySelector('div:nth-child(3)');
  153. if (specificVocabularyElement) {
  154. vocabulary = specificVocabularyElement.textContent.trim();
  155. }
  156.  
  157. const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
  158. if (kanjiRegex.test(vocabulary) || vocabulary) {
  159. return vocabulary;
  160. }
  161. } else if (kindText === 'Kanji') {
  162. const hiddenInput = document.querySelector('input[name="c"]');
  163. if (!hiddenInput) return '';
  164.  
  165. const vocab = hiddenInput.value.split(',')[1];
  166. const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
  167. if (kanjiRegex.test(vocab)) {
  168. return vocab;
  169. }
  170. }
  171. return '';
  172. }
  173.  
  174. function parseVocabFromVocabulary() {
  175. const url = window.location.href;
  176. const match = url.match(/https:\/\/jpdb\.io\/vocabulary\/(\d+)\/([^\#]*)#a/);
  177. if (match) {
  178. let vocab = match[2];
  179. state.embedAboveSubsectionMeanings = true;
  180. vocab = vocab.split('/')[0];
  181. return decodeURIComponent(vocab);
  182. }
  183. return '';
  184. }
  185.  
  186. function parseVocabFromKanji() {
  187. const url = window.location.href;
  188. const match = url.match(/https:\/\/jpdb\.io\/kanji\/([^#]*)#a/);
  189. if (match) {
  190. return decodeURIComponent(match[1]);
  191. }
  192. return '';
  193. }
  194.  
  195. function highlightVocab(sentence, vocab) {
  196. if (!CONFIG.COLORED_SENTENCE_TEXT) return sentence;
  197.  
  198. if (state.exactSearch) {
  199. const regex = new RegExp(`(${vocab})`, 'g');
  200. return sentence.replace(regex, '<span style="color: var(--outline-input-color);">$1</span>');
  201. } else {
  202. return vocab.split('').reduce((acc, char) => {
  203. const regex = new RegExp(char, 'g');
  204. return acc.replace(regex, `<span style="color: var(--outline-input-color);">${char}</span>`);
  205. }, sentence);
  206. }
  207. }
  208.  
  209. function createIconLink(iconClass, onClick, marginLeft = '0') {
  210. const link = document.createElement('a');
  211. link.href = '#';
  212. link.style.border = '0';
  213. link.style.display = 'inline-flex';
  214. link.style.verticalAlign = 'middle';
  215. link.style.marginLeft = marginLeft;
  216.  
  217. const icon = document.createElement('i');
  218. icon.className = iconClass;
  219. icon.style.fontSize = '1.4rem';
  220. icon.style.opacity = '1.0';
  221. icon.style.verticalAlign = 'baseline';
  222. icon.style.color = '#3d81ff';
  223.  
  224. link.appendChild(icon);
  225. link.addEventListener('click', onClick);
  226. return link;
  227. }
  228.  
  229. function createQuoteButton() {
  230. const link = document.createElement('a');
  231. link.href = '#';
  232. link.style.border = '0';
  233. link.style.display = 'inline-flex';
  234. link.style.verticalAlign = 'middle';
  235. link.style.marginLeft = '0rem';
  236.  
  237. const quoteIcon = document.createElement('span');
  238. quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』';
  239. quoteIcon.style.fontSize = '1.1rem';
  240. quoteIcon.style.color = '#3D8DFF';
  241. quoteIcon.style.verticalAlign = 'middle';
  242. quoteIcon.style.position = 'relative';
  243. quoteIcon.style.top = '0px';
  244.  
  245. link.appendChild(quoteIcon);
  246. link.addEventListener('click', (event) => {
  247. event.preventDefault();
  248. state.exactSearch = !state.exactSearch;
  249. quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』';
  250.  
  251. const storedData = getStoredData(state.vocab);
  252. if (storedData && storedData.exactState === state.exactSearch) {
  253. state.currentExampleIndex = storedData.index;
  254. } else {
  255. state.currentExampleIndex = 0;
  256. }
  257.  
  258. getImmersionKitData(state.vocab, state.exactSearch, () => {
  259. embedImageAndPlayAudio();
  260. });
  261. });
  262. return link;
  263. }
  264.  
  265. function createStarLink() {
  266. const link = document.createElement('a');
  267. link.href = '#';
  268. link.style.border = '0';
  269. link.style.display = 'inline-flex';
  270. link.style.verticalAlign = 'middle';
  271.  
  272. const storedValue = localStorage.getItem(state.vocab);
  273. const starIcon = document.createElement('span');
  274.  
  275. if (!storedValue) {
  276. starIcon.textContent = '☆';
  277. } else {
  278. const [storedIndex, storedExactState] = storedValue.split(',');
  279. const index = parseInt(storedIndex, 10);
  280. const exactState = storedExactState === '1';
  281. starIcon.textContent = (state.currentExampleIndex === index && state.exactSearch === exactState) ? '★' : '☆';
  282. }
  283.  
  284. starIcon.style.fontSize = '1.4rem';
  285. starIcon.style.marginLeft = '0.5rem';
  286. starIcon.style.color = '#3D8DFF';
  287. starIcon.style.verticalAlign = 'middle';
  288. starIcon.style.position = 'relative';
  289. starIcon.style.top = '-2px';
  290.  
  291. link.appendChild(starIcon);
  292. link.addEventListener('click', (event) => {
  293. event.preventDefault();
  294. const storedValue = localStorage.getItem(state.vocab);
  295. if (storedValue) {
  296. const [storedIndex, storedExactState] = storedValue.split(',');
  297. const index = parseInt(storedIndex, 10);
  298. const exactState = storedExactState === '1';
  299. if (index === state.currentExampleIndex && exactState === state.exactSearch) {
  300. localStorage.removeItem(state.vocab);
  301. } else {
  302. localStorage.setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`);
  303. }
  304. } else {
  305. localStorage.setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`);
  306. }
  307. // Refresh the embed without playing the audio
  308. renderImageAndPlayAudio(state.vocab, false);
  309. });
  310. return link;
  311. }
  312.  
  313. function createExportImportButtons() {
  314. const exportButton = document.createElement('button');
  315. exportButton.textContent = 'Export Favorites';
  316. exportButton.style.marginRight = '10px';
  317. exportButton.addEventListener('click', exportFavorites);
  318.  
  319. const importButton = document.createElement('button');
  320. importButton.textContent = 'Import Favorites';
  321. importButton.addEventListener('click', () => {
  322. const fileInput = document.createElement('input');
  323. fileInput.type = 'file';
  324. fileInput.accept = 'application/json';
  325. fileInput.addEventListener('change', importFavorites);
  326. fileInput.click();
  327. });
  328.  
  329. const buttonContainer = document.createElement('div');
  330. buttonContainer.style.textAlign = 'center';
  331. buttonContainer.style.marginTop = '10px';
  332. buttonContainer.append(exportButton, importButton);
  333.  
  334. document.body.appendChild(buttonContainer);
  335. }
  336.  
  337. function playAudio(soundUrl) {
  338. if (soundUrl) {
  339. // Stop the current audio if it's playing
  340. if (state.currentAudio) {
  341. state.currentAudio.pause();
  342. state.currentAudio.src = '';
  343. }
  344.  
  345. // Fetch the audio file as a blob and create a blob URL
  346. GM_xmlhttpRequest({
  347. method: 'GET',
  348. url: soundUrl,
  349. responseType: 'blob',
  350. onload: function(response) {
  351. const blobUrl = URL.createObjectURL(response.response);
  352. const audioElement = new Audio(blobUrl);
  353. audioElement.volume = CONFIG.SOUND_VOLUME; // Adjust volume as needed
  354. audioElement.play();
  355. state.currentAudio = audioElement; // Keep reference to the current audio
  356. },
  357. onerror: function(error) {
  358. console.error('Error fetching audio:', error);
  359. }
  360. });
  361. }
  362. }
  363.  
  364. function renderImageAndPlayAudio(vocab, shouldAutoPlaySound) {
  365. const example = state.examples[state.currentExampleIndex] || {};
  366. const imageUrl = example.image_url || null;
  367. const soundUrl = example.sound_url || null;
  368. const sentence = example.sentence || null;
  369.  
  370. const resultVocabularySection = document.querySelector('.result.vocabulary');
  371. const hboxWrapSection = document.querySelector('.hbox.wrap');
  372. const subsectionMeanings = document.querySelector('.subsection-meanings');
  373. const subsectionComposedOfKanji = document.querySelector('.subsection-composed-of-kanji');
  374. const subsectionPitchAccent = document.querySelector('.subsection-pitch-accent');
  375. const subsectionLabels = document.querySelectorAll('h6.subsection-label');
  376. const vboxGap = document.querySelector('.vbox.gap');
  377.  
  378. // Remove any existing container before creating a new one
  379. const existingContainer = document.getElementById('immersion-kit-container');
  380. if (existingContainer) {
  381. existingContainer.remove();
  382. }
  383.  
  384. if (resultVocabularySection || hboxWrapSection || subsectionMeanings || subsectionLabels.length >= 3) {
  385. const wrapperDiv = document.createElement('div');
  386. wrapperDiv.id = 'image-wrapper';
  387. wrapperDiv.style.textAlign = 'center';
  388. wrapperDiv.style.padding = '5px 0';
  389.  
  390. const textDiv = document.createElement('div');
  391. textDiv.style.marginBottom = '5px';
  392. textDiv.style.lineHeight = '1.4rem';
  393.  
  394. const contentText = document.createElement('span');
  395. contentText.textContent = 'Immersion Kit';
  396. contentText.style.color = 'var(--subsection-label-color)';
  397. contentText.style.fontSize = '85%';
  398. contentText.style.marginRight = '0.5rem';
  399. contentText.style.verticalAlign = 'middle';
  400.  
  401. const speakerLink = createIconLink('ti ti-volume', (event) => {
  402. event.preventDefault();
  403. playAudio(soundUrl);
  404. }, '0.5rem');
  405.  
  406. const starLink = createStarLink();
  407. const quoteButton = createQuoteButton(); // Create the quote button
  408.  
  409. textDiv.append(contentText, speakerLink, starLink, quoteButton); // Append the quote button
  410. wrapperDiv.appendChild(textDiv);
  411.  
  412. if (imageUrl) {
  413. const imageElement = GM_addElement(wrapperDiv, 'img', {
  414. src: imageUrl,
  415. alt: 'Embedded Image',
  416. style: `max-width: ${CONFIG.IMAGE_WIDTH}; margin-top: 10px; cursor: pointer;`
  417. });
  418.  
  419. if (imageElement) {
  420. imageElement.addEventListener('click', () => {
  421. speakerLink.click();
  422. });
  423. }
  424.  
  425. if (sentence) {
  426. const sentenceText = document.createElement('div');
  427. sentenceText.innerHTML = highlightVocab(sentence, vocab);
  428. sentenceText.style.marginTop = '10px';
  429. sentenceText.style.fontSize = CONFIG.SENTENCE_FONT_SIZE;
  430. sentenceText.style.color = 'lightgray';
  431. sentenceText.style.maxWidth = CONFIG.IMAGE_WIDTH;
  432. sentenceText.style.whiteSpace = 'pre-wrap';
  433. wrapperDiv.appendChild(sentenceText);
  434.  
  435. if (CONFIG.ENABLE_EXAMPLE_TRANSLATION && example.translation) {
  436. const translationText = document.createElement('div');
  437. translationText.innerHTML = replaceSpecialCharacters(example.translation);
  438. translationText.style.marginTop = '5px';
  439. translationText.style.fontSize = CONFIG.TRANSLATION_FONT_SIZE;
  440. translationText.style.color = 'var(--subsection-label-color)';
  441. translationText.style.maxWidth = CONFIG.IMAGE_WIDTH;
  442. translationText.style.whiteSpace = 'pre-wrap';
  443. wrapperDiv.appendChild(translationText);
  444. }
  445. } else {
  446. const noneText = document.createElement('div');
  447. noneText.textContent = 'None';
  448. noneText.style.marginTop = '10px';
  449. noneText.style.fontSize = '85%';
  450. noneText.style.color = 'var(--subsection-label-color)';
  451. wrapperDiv.appendChild(noneText);
  452. }
  453. }
  454.  
  455. // Create a fixed-width container for arrows and image
  456. const navigationDiv = document.createElement('div');
  457. navigationDiv.id = 'immersion-kit-embed';
  458. navigationDiv.style.display = 'flex';
  459. navigationDiv.style.justifyContent = 'center';
  460. navigationDiv.style.alignItems = 'center';
  461. navigationDiv.style.maxWidth = CONFIG.IMAGE_WIDTH;
  462. navigationDiv.style.margin = '0 auto';
  463.  
  464. const leftArrow = document.createElement('button');
  465. leftArrow.textContent = '<';
  466. leftArrow.style.marginRight = '10px';
  467. leftArrow.disabled = state.currentExampleIndex === 0;
  468. leftArrow.addEventListener('click', () => {
  469. if (state.currentExampleIndex > 0) {
  470. state.currentExampleIndex--;
  471. renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
  472. preloadImages();
  473. }
  474. });
  475.  
  476. const rightArrow = document.createElement('button');
  477. rightArrow.textContent = '>';
  478. rightArrow.style.marginLeft = '10px';
  479. rightArrow.disabled = state.currentExampleIndex >= state.examples.length - 1;
  480. rightArrow.addEventListener('click', () => {
  481. if (state.currentExampleIndex < state.examples.length - 1) {
  482. state.currentExampleIndex++;
  483. renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
  484. preloadImages();
  485. }
  486. });
  487.  
  488. const embedWrapper = document.createElement('div');
  489. embedWrapper.style.flex = '0 0 auto';
  490. embedWrapper.style.marginLeft = '10px';
  491. embedWrapper.appendChild(navigationDiv);
  492.  
  493. const containerDiv = document.createElement('div');
  494. containerDiv.id = 'immersion-kit-container'; // Added ID for targeting
  495. containerDiv.style.display = 'flex';
  496. containerDiv.style.alignItems = 'center';
  497. containerDiv.style.justifyContent = CONFIG.WIDE_MODE ? 'flex-start' : 'center'; // Center if wide_mode is false
  498. containerDiv.append(leftArrow, wrapperDiv, rightArrow, embedWrapper);
  499.  
  500. if (CONFIG.WIDE_MODE && subsectionMeanings) {
  501. const wrapper = document.createElement('div');
  502. wrapper.style.display = 'flex';
  503. wrapper.style.alignItems = 'flex-start';
  504.  
  505. const originalContentWrapper = document.createElement('div');
  506. originalContentWrapper.style.flex = '1';
  507. originalContentWrapper.appendChild(subsectionMeanings);
  508.  
  509. // Append the new subsections with a newline before each
  510. if (subsectionComposedOfKanji) {
  511. const newline1 = document.createElement('br');
  512. originalContentWrapper.appendChild(newline1);
  513. originalContentWrapper.appendChild(subsectionComposedOfKanji);
  514. }
  515. if (subsectionPitchAccent) {
  516. const newline2 = document.createElement('br');
  517. originalContentWrapper.appendChild(newline2);
  518. originalContentWrapper.appendChild(subsectionPitchAccent);
  519. }
  520.  
  521. wrapper.appendChild(originalContentWrapper);
  522. wrapper.appendChild(containerDiv);
  523.  
  524. if (vboxGap) {
  525. // Remove only the dynamically added div under vboxGap
  526. const existingDynamicDiv = vboxGap.querySelector('#dynamic-content');
  527. if (existingDynamicDiv) {
  528. existingDynamicDiv.remove();
  529. }
  530.  
  531. const dynamicDiv = document.createElement('div');
  532. dynamicDiv.id = 'dynamic-content';
  533. dynamicDiv.appendChild(wrapper);
  534.  
  535. // Insert after the first child if the URL contains vocabulary
  536. if (window.location.href.includes('vocabulary')) {
  537. vboxGap.insertBefore(dynamicDiv, vboxGap.children[1]);
  538. } else {
  539. vboxGap.insertBefore(dynamicDiv, vboxGap.firstChild); // Insert at the top
  540. }
  541. }
  542. } else {
  543. if (state.embedAboveSubsectionMeanings && subsectionMeanings) {
  544. subsectionMeanings.parentNode.insertBefore(containerDiv, subsectionMeanings);
  545. } else if (resultVocabularySection) {
  546. resultVocabularySection.parentNode.insertBefore(containerDiv, resultVocabularySection);
  547. } else if (hboxWrapSection) {
  548. hboxWrapSection.parentNode.insertBefore(containerDiv, hboxWrapSection);
  549. } else if (subsectionLabels.length >= 4) {
  550. subsectionLabels[3].parentNode.insertBefore(containerDiv, subsectionLabels[3]);
  551. }
  552. }
  553. }
  554.  
  555. if (CONFIG.AUTO_PLAY_SOUND && shouldAutoPlaySound) {
  556. playAudio(soundUrl);
  557. }
  558. }
  559.  
  560. function embedImageAndPlayAudio() {
  561. const existingNavigationDiv = document.getElementById('immersion-kit-embed');
  562. if (existingNavigationDiv) existingNavigationDiv.remove();
  563.  
  564. const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
  565.  
  566. if (state.vocab && !state.apiDataFetched) {
  567. getImmersionKitData(state.vocab, state.exactSearch, () => {
  568. renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
  569. preloadImages();
  570. });
  571. } else {
  572. renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
  573. preloadImages();
  574. }
  575. }
  576.  
  577. function replaceSpecialCharacters(text) {
  578. return text.replace(/<br>/g, '\n').replace(/&quot;/g, '"').replace(/\n/g, '<br>');
  579. }
  580.  
  581. function preloadImages() {
  582. const preloadDiv = GM_addElement(document.body, 'div', { style: 'display: none;' });
  583.  
  584. for (let i = Math.max(0, state.currentExampleIndex - CONFIG.NUM_PRELOADS); i <= Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUM_PRELOADS); i++) {
  585. if (!state.preloadedIndices.has(i)) {
  586. const example = state.examples[i];
  587. if (example.image_url) {
  588. GM_addElement(preloadDiv, 'img', { src: example.image_url });
  589. state.preloadedIndices.add(i);
  590. }
  591. }
  592. }
  593. }
  594.  
  595. function onUrlChange() {
  596. state.embedAboveSubsectionMeanings = false;
  597. if (window.location.href.includes('/vocabulary/')) {
  598. state.vocab = parseVocabFromVocabulary();
  599. } else if (window.location.href.includes('c=')) {
  600. state.vocab = parseVocabFromAnswer();
  601. } else if (window.location.href.includes('/kanji/')) {
  602. state.vocab = parseVocabFromKanji();
  603. } else {
  604. state.vocab = parseVocabFromReview();
  605. }
  606.  
  607. const storedData = getStoredData(state.vocab);
  608. state.currentExampleIndex = storedData.index;
  609. state.exactSearch = storedData.exactState;
  610.  
  611. const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
  612. const shouldAutoPlaySound = !reviewUrlPattern.test(window.location.href);
  613.  
  614. if (state.vocab) {
  615. embedImageAndPlayAudio();
  616. }
  617. }
  618.  
  619. const observer = new MutationObserver(() => {
  620. if (window.location.href !== observer.lastUrl) {
  621. observer.lastUrl = window.location.href;
  622. onUrlChange();
  623.  
  624. setPageWidth();
  625. }
  626. });
  627.  
  628. function setPageWidth(){
  629. if (CONFIG.WIDE_MODE) {
  630. document.body.style.maxWidth = CONFIG.PAGE_MAX_WIDTH;
  631. } };
  632.  
  633. observer.lastUrl = window.location;
  634. observer.lastUrl = window.location.href;
  635. observer.observe(document, { subtree: true, childList: true });
  636.  
  637. window.addEventListener('load', onUrlChange);
  638. window.addEventListener('popstate', onUrlChange);
  639. window.addEventListener('hashchange', onUrlChange);
  640.  
  641.  
  642. setPageWidth();
  643. createExportImportButtons();
  644. })();