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.4
  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 storedData = getStoredData(state.vocab);
  273. const starIcon = document.createElement('span');
  274.  
  275. if (!localStorage.getItem(state.vocab)) {
  276. starIcon.textContent = '☆';
  277. } else {
  278. starIcon.textContent = (state.currentExampleIndex === storedData.index && state.exactSearch === storedData.exactState) ? '★' : '☆';
  279. }
  280.  
  281.  
  282. starIcon.style.fontSize = '1.4rem';
  283. starIcon.style.marginLeft = '0.5rem';
  284. starIcon.style.color = '#3D8DFF';
  285. starIcon.style.verticalAlign = 'middle';
  286. starIcon.style.position = 'relative';
  287. starIcon.style.top = '-2px';
  288.  
  289. link.appendChild(starIcon);
  290. link.addEventListener('click', (event) => {
  291. event.preventDefault();
  292. const favoriteData = getStoredData(state.vocab);
  293. if (favoriteData && favoriteData.index === state.currentExampleIndex && favoriteData.exactState === state.exactSearch) {
  294. localStorage.removeItem(state.vocab);
  295. } else {
  296. storeData(state.vocab, state.currentExampleIndex, state.exactSearch);
  297. }
  298. embedImageAndPlayAudio();
  299. });
  300. return link;
  301. }
  302.  
  303. function createExportImportButtons() {
  304. const exportButton = document.createElement('button');
  305. exportButton.textContent = 'Export Favorites';
  306. exportButton.style.marginRight = '10px';
  307. exportButton.addEventListener('click', exportFavorites);
  308.  
  309. const importButton = document.createElement('button');
  310. importButton.textContent = 'Import Favorites';
  311. importButton.addEventListener('click', () => {
  312. const fileInput = document.createElement('input');
  313. fileInput.type = 'file';
  314. fileInput.accept = 'application/json';
  315. fileInput.addEventListener('change', importFavorites);
  316. fileInput.click();
  317. });
  318.  
  319. const buttonContainer = document.createElement('div');
  320. buttonContainer.style.textAlign = 'center';
  321. buttonContainer.style.marginTop = '10px';
  322. buttonContainer.append(exportButton, importButton);
  323.  
  324. document.body.appendChild(buttonContainer);
  325. }
  326.  
  327. function playAudio(soundUrl) {
  328. if (soundUrl) {
  329. // Stop the current audio if it's playing
  330. if (state.currentAudio) {
  331. state.currentAudio.pause();
  332. state.currentAudio.src = '';
  333. }
  334.  
  335. // Fetch the audio file as a blob and create a blob URL
  336. GM_xmlhttpRequest({
  337. method: 'GET',
  338. url: soundUrl,
  339. responseType: 'blob',
  340. onload: function(response) {
  341. const blobUrl = URL.createObjectURL(response.response);
  342. const audioElement = new Audio(blobUrl);
  343. audioElement.volume = CONFIG.SOUND_VOLUME; // Adjust volume as needed
  344. audioElement.play();
  345. state.currentAudio = audioElement; // Keep reference to the current audio
  346. },
  347. onerror: function(error) {
  348. console.error('Error fetching audio:', error);
  349. }
  350. });
  351. }
  352. }
  353.  
  354. function renderImageAndPlayAudio(vocab, shouldAutoPlaySound) {
  355. const example = state.examples[state.currentExampleIndex] || {};
  356. const imageUrl = example.image_url || null;
  357. const soundUrl = example.sound_url || null;
  358. const sentence = example.sentence || null;
  359.  
  360. const resultVocabularySection = document.querySelector('.result.vocabulary');
  361. const hboxWrapSection = document.querySelector('.hbox.wrap');
  362. const subsectionMeanings = document.querySelector('.subsection-meanings');
  363. const subsectionComposedOfKanji = document.querySelector('.subsection-composed-of-kanji');
  364. const subsectionPitchAccent = document.querySelector('.subsection-pitch-accent');
  365. const subsectionLabels = document.querySelectorAll('h6.subsection-label');
  366. const vboxGap = document.querySelector('.vbox.gap');
  367.  
  368. // Remove any existing container before creating a new one
  369. const existingContainer = document.getElementById('immersion-kit-container');
  370. if (existingContainer) {
  371. existingContainer.remove();
  372. }
  373.  
  374. if (resultVocabularySection || hboxWrapSection || subsectionMeanings || subsectionLabels.length >= 3) {
  375. const wrapperDiv = document.createElement('div');
  376. wrapperDiv.id = 'image-wrapper';
  377. wrapperDiv.style.textAlign = 'center';
  378. wrapperDiv.style.padding = '5px 0';
  379.  
  380. const textDiv = document.createElement('div');
  381. textDiv.style.marginBottom = '5px';
  382. textDiv.style.lineHeight = '1.4rem';
  383.  
  384. const contentText = document.createElement('span');
  385. contentText.textContent = 'Immersion Kit';
  386. contentText.style.color = 'var(--subsection-label-color)';
  387. contentText.style.fontSize = '85%';
  388. contentText.style.marginRight = '0.5rem';
  389. contentText.style.verticalAlign = 'middle';
  390.  
  391. const speakerLink = createIconLink('ti ti-volume', (event) => {
  392. event.preventDefault();
  393. playAudio(soundUrl);
  394. }, '0.5rem');
  395.  
  396. const starLink = createStarLink();
  397. const quoteButton = createQuoteButton(); // Create the quote button
  398.  
  399. textDiv.append(contentText, speakerLink, starLink, quoteButton); // Append the quote button
  400. wrapperDiv.appendChild(textDiv);
  401.  
  402. if (imageUrl) {
  403. const imageElement = GM_addElement(wrapperDiv, 'img', {
  404. src: imageUrl,
  405. alt: 'Embedded Image',
  406. style: `max-width: ${CONFIG.IMAGE_WIDTH}; margin-top: 10px; cursor: pointer;`
  407. });
  408.  
  409. if (imageElement) {
  410. imageElement.addEventListener('click', () => {
  411. speakerLink.click();
  412. });
  413. }
  414.  
  415. if (sentence) {
  416. const sentenceText = document.createElement('div');
  417. sentenceText.innerHTML = highlightVocab(sentence, vocab);
  418. sentenceText.style.marginTop = '10px';
  419. sentenceText.style.fontSize = CONFIG.SENTENCE_FONT_SIZE;
  420. sentenceText.style.color = 'lightgray';
  421. sentenceText.style.maxWidth = CONFIG.IMAGE_WIDTH;
  422. sentenceText.style.whiteSpace = 'pre-wrap';
  423. wrapperDiv.appendChild(sentenceText);
  424.  
  425. if (CONFIG.ENABLE_EXAMPLE_TRANSLATION && example.translation) {
  426. const translationText = document.createElement('div');
  427. translationText.innerHTML = replaceSpecialCharacters(example.translation);
  428. translationText.style.marginTop = '5px';
  429. translationText.style.fontSize = CONFIG.TRANSLATION_FONT_SIZE;
  430. translationText.style.color = 'var(--subsection-label-color)';
  431. translationText.style.maxWidth = CONFIG.IMAGE_WIDTH;
  432. translationText.style.whiteSpace = 'pre-wrap';
  433. wrapperDiv.appendChild(translationText);
  434. }
  435. } else {
  436. const noneText = document.createElement('div');
  437. noneText.textContent = 'None';
  438. noneText.style.marginTop = '10px';
  439. noneText.style.fontSize = '85%';
  440. noneText.style.color = 'var(--subsection-label-color)';
  441. wrapperDiv.appendChild(noneText);
  442. }
  443. }
  444.  
  445. // Create a fixed-width container for arrows and image
  446. const navigationDiv = document.createElement('div');
  447. navigationDiv.id = 'immersion-kit-embed';
  448. navigationDiv.style.display = 'flex';
  449. navigationDiv.style.justifyContent = 'center';
  450. navigationDiv.style.alignItems = 'center';
  451. navigationDiv.style.maxWidth = CONFIG.IMAGE_WIDTH;
  452. navigationDiv.style.margin = '0 auto';
  453.  
  454. const leftArrow = document.createElement('button');
  455. leftArrow.textContent = '<';
  456. leftArrow.style.marginRight = '10px';
  457. leftArrow.disabled = state.currentExampleIndex === 0;
  458. leftArrow.addEventListener('click', () => {
  459. if (state.currentExampleIndex > 0) {
  460. state.currentExampleIndex--;
  461. renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
  462. preloadImages();
  463. }
  464. });
  465.  
  466. const rightArrow = document.createElement('button');
  467. rightArrow.textContent = '>';
  468. rightArrow.style.marginLeft = '10px';
  469. rightArrow.disabled = state.currentExampleIndex >= state.examples.length - 1;
  470. rightArrow.addEventListener('click', () => {
  471. if (state.currentExampleIndex < state.examples.length - 1) {
  472. state.currentExampleIndex++;
  473. renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
  474. preloadImages();
  475. }
  476. });
  477.  
  478. const embedWrapper = document.createElement('div');
  479. embedWrapper.style.flex = '0 0 auto';
  480. embedWrapper.style.marginLeft = '10px';
  481. embedWrapper.appendChild(navigationDiv);
  482.  
  483. const containerDiv = document.createElement('div');
  484. containerDiv.id = 'immersion-kit-container'; // Added ID for targeting
  485. containerDiv.style.display = 'flex';
  486. containerDiv.style.alignItems = 'center';
  487. containerDiv.style.justifyContent = CONFIG.WIDE_MODE ? 'flex-start' : 'center'; // Center if wide_mode is false
  488. containerDiv.append(leftArrow, wrapperDiv, rightArrow, embedWrapper);
  489.  
  490. if (CONFIG.WIDE_MODE && subsectionMeanings) {
  491. const wrapper = document.createElement('div');
  492. wrapper.style.display = 'flex';
  493. wrapper.style.alignItems = 'flex-start';
  494.  
  495. const originalContentWrapper = document.createElement('div');
  496. originalContentWrapper.style.flex = '1';
  497. originalContentWrapper.appendChild(subsectionMeanings);
  498.  
  499. // Append the new subsections with a newline before each
  500. if (subsectionComposedOfKanji) {
  501. const newline1 = document.createElement('br');
  502. originalContentWrapper.appendChild(newline1);
  503. originalContentWrapper.appendChild(subsectionComposedOfKanji);
  504. }
  505. if (subsectionPitchAccent) {
  506. const newline2 = document.createElement('br');
  507. originalContentWrapper.appendChild(newline2);
  508. originalContentWrapper.appendChild(subsectionPitchAccent);
  509. }
  510.  
  511. wrapper.appendChild(originalContentWrapper);
  512. wrapper.appendChild(containerDiv);
  513.  
  514. if (vboxGap) {
  515. // Remove only the dynamically added div under vboxGap
  516. const existingDynamicDiv = vboxGap.querySelector('#dynamic-content');
  517. if (existingDynamicDiv) {
  518. existingDynamicDiv.remove();
  519. }
  520.  
  521. const dynamicDiv = document.createElement('div');
  522. dynamicDiv.id = 'dynamic-content';
  523. dynamicDiv.appendChild(wrapper);
  524.  
  525. // Insert after the first child if the URL contains vocabulary
  526. if (window.location.href.includes('vocabulary')) {
  527. vboxGap.insertBefore(dynamicDiv, vboxGap.children[1]);
  528. } else {
  529. vboxGap.insertBefore(dynamicDiv, vboxGap.firstChild); // Insert at the top
  530. }
  531. }
  532. } else {
  533. if (state.embedAboveSubsectionMeanings && subsectionMeanings) {
  534. subsectionMeanings.parentNode.insertBefore(containerDiv, subsectionMeanings);
  535. } else if (resultVocabularySection) {
  536. resultVocabularySection.parentNode.insertBefore(containerDiv, resultVocabularySection);
  537. } else if (hboxWrapSection) {
  538. hboxWrapSection.parentNode.insertBefore(containerDiv, hboxWrapSection);
  539. } else if (subsectionLabels.length >= 4) {
  540. subsectionLabels[3].parentNode.insertBefore(containerDiv, subsectionLabels[3]);
  541. }
  542. }
  543. }
  544.  
  545. if (CONFIG.AUTO_PLAY_SOUND && shouldAutoPlaySound) {
  546. playAudio(soundUrl);
  547. }
  548. }
  549.  
  550. function embedImageAndPlayAudio() {
  551. const existingNavigationDiv = document.getElementById('immersion-kit-embed');
  552. if (existingNavigationDiv) existingNavigationDiv.remove();
  553.  
  554. const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
  555.  
  556. if (state.vocab && !state.apiDataFetched) {
  557. getImmersionKitData(state.vocab, state.exactSearch, () => {
  558. renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
  559. preloadImages();
  560. });
  561. } else {
  562. renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
  563. preloadImages();
  564. }
  565. }
  566.  
  567. function replaceSpecialCharacters(text) {
  568. return text.replace(/<br>/g, '\n').replace(/&quot;/g, '"').replace(/\n/g, '<br>');
  569. }
  570.  
  571. function preloadImages() {
  572. const preloadDiv = GM_addElement(document.body, 'div', { style: 'display: none;' });
  573.  
  574. for (let i = Math.max(0, state.currentExampleIndex - CONFIG.NUM_PRELOADS); i <= Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUM_PRELOADS); i++) {
  575. if (!state.preloadedIndices.has(i)) {
  576. const example = state.examples[i];
  577. if (example.image_url) {
  578. GM_addElement(preloadDiv, 'img', { src: example.image_url });
  579. state.preloadedIndices.add(i);
  580. }
  581. }
  582. }
  583. }
  584.  
  585. function onUrlChange() {
  586. state.embedAboveSubsectionMeanings = false;
  587. if (window.location.href.includes('/vocabulary/')) {
  588. state.vocab = parseVocabFromVocabulary();
  589. } else if (window.location.href.includes('c=')) {
  590. state.vocab = parseVocabFromAnswer();
  591. } else if (window.location.href.includes('/kanji/')) {
  592. state.vocab = parseVocabFromKanji();
  593. } else {
  594. state.vocab = parseVocabFromReview();
  595. }
  596.  
  597. const storedData = getStoredData(state.vocab);
  598. state.currentExampleIndex = storedData.index;
  599. state.exactSearch = storedData.exactState;
  600.  
  601. const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
  602. const shouldAutoPlaySound = !reviewUrlPattern.test(window.location.href);
  603.  
  604. if (state.vocab) {
  605. embedImageAndPlayAudio();
  606. }
  607. }
  608.  
  609. const observer = new MutationObserver(() => {
  610. if (window.location.href !== observer.lastUrl) {
  611. observer.lastUrl = window.location.href;
  612. onUrlChange();
  613.  
  614. setPageWidth();
  615. }
  616. });
  617.  
  618. function setPageWidth(){
  619. if (CONFIG.WIDE_MODE) {
  620. document.body.style.maxWidth = CONFIG.PAGE_MAX_WIDTH;
  621. } };
  622.  
  623. observer.lastUrl = window.location;
  624. observer.lastUrl = window.location.href;
  625. observer.observe(document, { subtree: true, childList: true });
  626.  
  627. window.addEventListener('load', onUrlChange);
  628. window.addEventListener('popstate', onUrlChange);
  629. window.addEventListener('hashchange', onUrlChange);
  630.  
  631.  
  632. setPageWidth();
  633. createExportImportButtons();
  634. })();