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-12-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name JPDB Immersion Kit Examples
  3. // @version 1.18
  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. // @match https://jpdb.io/search*
  11. // @connect immersionkit.com
  12. // @connect linodeobjects.com
  13. // @grant GM_addElement
  14. // @grant GM_xmlhttpRequest
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. const CONFIG = {
  22. IMAGE_WIDTH: '400px',
  23. WIDE_MODE: true,
  24. PAGE_WIDTH: '75rem',
  25. SOUND_VOLUME: 80,
  26. ENABLE_EXAMPLE_TRANSLATION: true,
  27. SENTENCE_FONT_SIZE: '120%',
  28. TRANSLATION_FONT_SIZE: '85%',
  29. COLORED_SENTENCE_TEXT: true,
  30. AUTO_PLAY_SOUND: true,
  31. NUMBER_OF_PRELOADS: 1,
  32. VOCAB_SIZE: '250%',
  33. MINIMUM_EXAMPLE_LENGTH: 0
  34. };
  35.  
  36. const state = {
  37. currentExampleIndex: 0,
  38. examples: [],
  39. apiDataFetched: false,
  40. vocab: '',
  41. embedAboveSubsectionMeanings: false,
  42. preloadedIndices: new Set(),
  43. currentAudio: null,
  44. exactSearch: true,
  45. error: false,
  46. currentlyPlayingAudio: false
  47. };
  48.  
  49.  
  50. // IndexedDB Manager
  51. const IndexedDBManager = {
  52. MAX_ENTRIES: 100000000,
  53. EXPIRATION_TIME: 30 * 24 * 60 * 60 * 1000 * 12 * 10000, // 10000 years in milliseconds
  54.  
  55. open() {
  56. return new Promise((resolve, reject) => {
  57. const request = indexedDB.open('ImmersionKitDB', 1);
  58. request.onupgradeneeded = function(event) {
  59. const db = event.target.result;
  60. if (!db.objectStoreNames.contains('dataStore')) {
  61. db.createObjectStore('dataStore', { keyPath: 'keyword' });
  62. }
  63. };
  64. request.onsuccess = function(event) {
  65. resolve(event.target.result);
  66. };
  67. request.onerror = function(event) {
  68. reject('IndexedDB error: ' + event.target.errorCode);
  69. };
  70. });
  71. },
  72.  
  73. get(db, keyword) {
  74. return new Promise((resolve, reject) => {
  75. const transaction = db.transaction(['dataStore'], 'readonly');
  76. const store = transaction.objectStore('dataStore');
  77. const request = store.get(keyword);
  78. request.onsuccess = async function(event) {
  79. const result = event.target.result;
  80. if (result) {
  81. const isExpired = Date.now() - result.timestamp >= this.EXPIRATION_TIME;
  82. const validationError = validateApiResponse(result.data);
  83.  
  84. if (isExpired) {
  85. console.log(`Deleting entry for keyword "${keyword}" because it is expired.`);
  86. await this.deleteEntry(db, keyword);
  87. resolve(null);
  88. } else if (validationError) {
  89. console.log(`Deleting entry for keyword "${keyword}" due to validation error: ${validationError}`);
  90. await this.deleteEntry(db, keyword);
  91. resolve(null);
  92. } else {
  93. resolve(result.data);
  94. }
  95. } else {
  96. resolve(null);
  97. }
  98. }.bind(this);
  99. request.onerror = function(event) {
  100. reject('IndexedDB get error: ' + event.target.errorCode);
  101. };
  102. });
  103. },
  104.  
  105. deleteEntry(db, keyword) {
  106. return new Promise((resolve, reject) => {
  107. const transaction = db.transaction(['dataStore'], 'readwrite');
  108. const store = transaction.objectStore('dataStore');
  109. const request = store.delete(keyword);
  110. request.onsuccess = () => resolve();
  111. request.onerror = (e) => reject('IndexedDB delete error: ' + e.target.errorCode);
  112. });
  113. },
  114.  
  115.  
  116. getAll(db) {
  117. return new Promise((resolve, reject) => {
  118. const transaction = db.transaction(['dataStore'], 'readonly');
  119. const store = transaction.objectStore('dataStore');
  120. const entries = [];
  121. store.openCursor().onsuccess = function(event) {
  122. const cursor = event.target.result;
  123. if (cursor) {
  124. entries.push(cursor.value);
  125. cursor.continue();
  126. } else {
  127. resolve(entries);
  128. }
  129. };
  130. store.openCursor().onerror = function(event) {
  131. reject('Failed to retrieve entries via cursor: ' + event.target.errorCode);
  132. };
  133. });
  134. },
  135.  
  136. save(db, keyword, data) {
  137. return new Promise(async (resolve, reject) => {
  138. try {
  139. const validationError = validateApiResponse(data);
  140. if (validationError) {
  141. console.log(`Invalid data detected: ${validationError}. Not saving to IndexedDB.`);
  142. resolve();
  143. return;
  144. }
  145.  
  146. const entries = await this.getAll(db);
  147. const transaction = db.transaction(['dataStore'], 'readwrite');
  148. const store = transaction.objectStore('dataStore');
  149.  
  150. if (entries.length >= this.MAX_ENTRIES) {
  151. // Sort entries by timestamp and delete oldest ones
  152. entries.sort((a, b) => a.timestamp - b.timestamp);
  153. const entriesToDelete = entries.slice(0, entries.length - this.MAX_ENTRIES + 1);
  154.  
  155. // Delete old entries
  156. entriesToDelete.forEach(entry => {
  157. store.delete(entry.keyword).onerror = function() {
  158. console.error('Failed to delete entry:', entry.keyword);
  159. };
  160. });
  161. }
  162.  
  163. // Add the new entry
  164. const addRequest = store.put({ keyword, data, timestamp: Date.now() });
  165. addRequest.onsuccess = () => resolve();
  166. addRequest.onerror = (e) => reject('IndexedDB save error: ' + e.target.errorCode);
  167.  
  168. transaction.oncomplete = function() {
  169. console.log('IndexedDB updated successfully.');
  170. };
  171.  
  172. transaction.onerror = function(event) {
  173. reject('IndexedDB update failed: ' + event.target.errorCode);
  174. };
  175.  
  176. } catch (error) {
  177. reject(`Error in saveToIndexedDB: ${error}`);
  178. }
  179. });
  180. },
  181.  
  182. delete() {
  183. return new Promise((resolve, reject) => {
  184. const request = indexedDB.deleteDatabase('ImmersionKitDB');
  185. request.onsuccess = function() {
  186. console.log('IndexedDB deleted successfully');
  187. resolve();
  188. };
  189. request.onerror = function(event) {
  190. console.error('Error deleting IndexedDB:', event.target.errorCode);
  191. reject('Error deleting IndexedDB: ' + event.target.errorCode);
  192. };
  193. request.onblocked = function() {
  194. console.warn('Delete operation blocked. Please close all other tabs with this site open and try again.');
  195. reject('Delete operation blocked');
  196. };
  197. });
  198. }
  199. };
  200.  
  201.  
  202. // API FUNCTIONS=====================================================================================================================
  203. function getImmersionKitData(vocab, exactSearch) {
  204. return new Promise(async (resolve, reject) => {
  205. const searchVocab = exactSearch ? `「${vocab}」` : vocab;
  206. const url = `https://api.immersionkit.com/look_up_dictionary?keyword=${encodeURIComponent(searchVocab)}&sort=shortness&min_length=${CONFIG.MINIMUM_EXAMPLE_LENGTH}`;
  207. const maxRetries = 5;
  208. let attempt = 0;
  209.  
  210. async function fetchData() {
  211. try {
  212. const db = await IndexedDBManager.open();
  213. const cachedData = await IndexedDBManager.get(db, searchVocab);
  214. if (cachedData && Array.isArray(cachedData.data) && cachedData.data.length > 0) {
  215. console.log('Data retrieved from IndexedDB');
  216. state.examples = cachedData.data[0].examples;
  217. state.apiDataFetched = true;
  218. resolve();
  219. } else {
  220. console.log(`Calling API for: ${searchVocab}`);
  221. GM_xmlhttpRequest({
  222. method: "GET",
  223. url: url,
  224. onload: async function(response) {
  225. if (response.status === 200) {
  226. const jsonData = parseJSON(response.responseText);
  227. console.log("API JSON Received");
  228. console.log(url);
  229. const validationError = validateApiResponse(jsonData);
  230. if (!validationError) {
  231. state.examples = jsonData.data[0].examples;
  232. state.apiDataFetched = true;
  233. await IndexedDBManager.save(db, searchVocab, jsonData);
  234. resolve();
  235. } else {
  236. attempt++;
  237. if (attempt < maxRetries) {
  238. console.log(`Validation error: ${validationError}. Retrying... (${attempt}/${maxRetries})`);
  239. fetchData();
  240. } else {
  241. reject(`Invalid API response after ${maxRetries} attempts: ${validationError}`);
  242. state.error = true;
  243. embedImageAndPlayAudio(); //update displayed text
  244. }
  245. }
  246. } else {
  247. reject(`API call failed with status: ${response.status}`);
  248. }
  249. },
  250. onerror: function(error) {
  251. reject(`An error occurred: ${error}`);
  252. }
  253. });
  254. }
  255. } catch (error) {
  256. reject(`Error: ${error}`);
  257. }
  258. }
  259.  
  260. fetchData();
  261. });
  262. }
  263.  
  264. function parseJSON(responseText) {
  265. try {
  266. return JSON.parse(responseText);
  267. } catch (e) {
  268. console.error('Error parsing JSON:', e);
  269. return null;
  270. }
  271. }
  272.  
  273. function validateApiResponse(jsonData) {
  274. state.error = false;
  275. if (!jsonData) {
  276. return 'Not a valid JSON';
  277. }
  278. if (!jsonData.data || !jsonData.data[0] || !jsonData.data[0].examples) {
  279. return 'Missing required data fields';
  280. }
  281.  
  282. const categoryCount = jsonData.data[0].category_count;
  283. if (!categoryCount) {
  284. return 'Missing category count';
  285. }
  286.  
  287. // Check if all category counts are zero
  288. const allZero = Object.values(categoryCount).every(count => count === 0);
  289. if (allZero) {
  290. return 'Blank API';
  291. }
  292.  
  293. return null; // No error
  294. }
  295.  
  296.  
  297. //FAVORITE DATA FUNCTIONS=====================================================================================================================
  298. function getStoredData(key) {
  299. // Retrieve the stored value from localStorage using the provided key
  300. const storedValue = localStorage.getItem(key);
  301.  
  302. // If a stored value exists, split it into index and exactState
  303. if (storedValue) {
  304. const [index, exactState] = storedValue.split(',');
  305. return {
  306. index: parseInt(index, 10), // Convert index to an integer
  307. exactState: exactState === '1' // Convert exactState to a boolean
  308. };
  309. }
  310.  
  311. // Return default values if no stored value exists
  312. return { index: 0, exactState: state.exactSearch };
  313. }
  314.  
  315. function storeData(key, index, exactState) {
  316. // Create a string value from index and exactState to store in localStorage
  317. const value = `${index},${exactState ? 1 : 0}`;
  318.  
  319. // Store the value in localStorage using the provided key
  320. localStorage.setItem(key, value);
  321. }
  322.  
  323.  
  324. // PARSE VOCAB FUNCTIONS =====================================================================================================================
  325. function parseVocabFromAnswer() {
  326. // Select all links containing "/kanji/" or "/vocabulary/" in the href attribute
  327. const elements = document.querySelectorAll('a[href*="/kanji/"], a[href*="/vocabulary/"]');
  328. console.log("Parsing Answer Page");
  329.  
  330. // Iterate through the matched elements
  331. for (const element of elements) {
  332. const href = element.getAttribute('href');
  333. const text = element.textContent.trim();
  334.  
  335. // Match the href to extract kanji or vocabulary (ignoring ID if present)
  336. const match = href.match(/\/(kanji|vocabulary)\/(?:\d+\/)?([^\#]*)#/);
  337. if (match) return match[2].trim();
  338. if (text) return text.trim();
  339. }
  340. return '';
  341. }
  342.  
  343. function parseVocabFromReview() {
  344. // Select the element with class 'kind' to determine the type of content
  345. const kindElement = document.querySelector('.kind');
  346. console.log("Parsing Review Page");
  347.  
  348. // If kindElement doesn't exist, set kindText to ''
  349. const kindText = kindElement ? kindElement.textContent.trim() : '';
  350.  
  351. // Accept 'Kanji', 'Vocabulary', or 'New' kindText
  352. if (kindText !== 'Kanji' && kindText !== 'Vocabulary' && kindText !== 'New') return ''; // Return empty if it's neither kanji nor vocab
  353.  
  354. if (kindText === 'Vocabulary' || kindText === 'New') {
  355. // Select the element with class 'plain' to extract vocabulary
  356. const plainElement = document.querySelector('.plain');
  357. if (!plainElement) return '';
  358.  
  359. let vocabulary = plainElement.textContent.trim();
  360. const nestedVocabularyElement = plainElement.querySelector('div:not([style])');
  361.  
  362. if (nestedVocabularyElement) {
  363. vocabulary = nestedVocabularyElement.textContent.trim();
  364. }
  365. const specificVocabularyElement = plainElement.querySelector('div:nth-child(3)');
  366.  
  367. if (specificVocabularyElement) {
  368. vocabulary = specificVocabularyElement.textContent.trim();
  369. }
  370.  
  371. // Regular expression to check if the vocabulary contains kanji characters
  372. const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
  373. if (kanjiRegex.test(vocabulary) || vocabulary) {
  374. console.log("Found Vocabulary:", vocabulary);
  375. return vocabulary;
  376. }
  377. } else if (kindText === 'Kanji') {
  378. // Select the hidden input element to extract kanji
  379. const hiddenInput = document.querySelector('input[name="c"]');
  380. if (!hiddenInput) return '';
  381.  
  382. const vocab = hiddenInput.value.split(',')[1];
  383. const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
  384. if (kanjiRegex.test(vocab)) {
  385. console.log("Found Kanji:", vocab);
  386. return vocab;
  387. }
  388. }
  389. return '';
  390. }
  391.  
  392. function parseVocabFromVocabulary() {
  393. // Get the current URL
  394. let url = window.location.href;
  395.  
  396. // Remove query parameters (e.g., ?lang=english) and fragment identifiers (#)
  397. url = url.split('?')[0].split('#')[0];
  398.  
  399. // Match the URL structure for a vocabulary page
  400. const match = url.match(/https:\/\/jpdb\.io\/vocabulary\/(\d+)\/([^\#\/]*)/);
  401. console.log("Parsing Vocabulary Page");
  402.  
  403. if (match) {
  404. // Extract and decode the vocabulary part from the URL
  405. let vocab = match[2];
  406. state.embedAboveSubsectionMeanings = true; // Set state flag
  407. return decodeURIComponent(vocab);
  408. }
  409.  
  410. // Return empty string if no match
  411. return '';
  412. }
  413.  
  414. function parseVocabFromKanji() {
  415. // Get the current URL
  416. const url = window.location.href;
  417.  
  418. // Match the URL structure for a kanji page
  419. const match = url.match(/https:\/\/jpdb\.io\/kanji\/(\d+)\/([^\#]*)#a/);
  420. console.log("Parsing Kanji Page");
  421.  
  422. if (match) {
  423. // Extract and decode the kanji part from the URL
  424. let kanji = match[2];
  425. state.embedAboveSubsectionMeanings = true; // Set state flag
  426. kanji = kanji.split('/')[0];
  427. return decodeURIComponent(kanji);
  428. }
  429.  
  430. // Return empty string if no match
  431. return '';
  432. }
  433.  
  434. function parseVocabFromSearch() {
  435. // Get the current URL
  436. let url = window.location.href;
  437.  
  438. // Match the URL structure for a search query, capturing the vocab between `?q=` and either `&` or `+`
  439. const match = url.match(/https:\/\/jpdb\.io\/search\?q=([^&+]*)/);
  440. console.log("Parsing Search Page");
  441.  
  442. if (match) {
  443. // Extract and decode the vocabulary part from the URL
  444. let vocab = match[1];
  445. return decodeURIComponent(vocab);
  446. }
  447.  
  448. // Return empty string if no match
  449. return '';
  450. }
  451.  
  452. //EMBED FUNCTIONS=====================================================================================================================
  453. function createAnchor(marginLeft) {
  454. // Create and style an anchor element
  455. const anchor = document.createElement('a');
  456. anchor.href = '#';
  457. anchor.style.border = '0';
  458. anchor.style.display = 'inline-flex';
  459. anchor.style.verticalAlign = 'middle';
  460. anchor.style.marginLeft = marginLeft;
  461. return anchor;
  462. }
  463.  
  464. function createIcon(iconClass, fontSize = '1.4rem', color = '#3d81ff') {
  465. // Create and style an icon element
  466. const icon = document.createElement('i');
  467. icon.className = iconClass;
  468. icon.style.fontSize = fontSize;
  469. icon.style.opacity = '1.0';
  470. icon.style.verticalAlign = 'baseline';
  471. icon.style.color = color;
  472. return icon;
  473. }
  474.  
  475. function createSpeakerButton(soundUrl) {
  476. // Create a speaker button with an icon and click event for audio playback
  477. const anchor = createAnchor('0.5rem');
  478. const icon = createIcon('ti ti-volume');
  479. anchor.appendChild(icon);
  480. anchor.addEventListener('click', (event) => {
  481. event.preventDefault();
  482. playAudio(soundUrl);
  483. });
  484. return anchor;
  485. }
  486.  
  487. function createStarButton() {
  488. // Create a star button with an icon and click event for toggling favorite state
  489. const anchor = createAnchor('0.5rem');
  490. const starIcon = document.createElement('span');
  491. const storedValue = localStorage.getItem(state.vocab);
  492.  
  493. // Determine the star icon (filled or empty) based on stored value
  494. if (!storedValue) {
  495. starIcon.textContent = '☆';
  496. } else {
  497. const [storedIndex, storedExactState] = storedValue.split(',');
  498. const index = parseInt(storedIndex, 10);
  499. const exactState = storedExactState === '1';
  500. starIcon.textContent = (state.currentExampleIndex === index && state.exactSearch === exactState) ? '★' : '☆';
  501. }
  502.  
  503. // Style the star icon
  504. starIcon.style.fontSize = '1.4rem';
  505. starIcon.style.color = '#3D8DFF';
  506. starIcon.style.verticalAlign = 'middle';
  507. starIcon.style.position = 'relative';
  508. starIcon.style.top = '-2px';
  509.  
  510. // Append the star icon to the anchor and set up the click event to toggle star state
  511. anchor.appendChild(starIcon);
  512. anchor.addEventListener('click', (event) => {
  513. event.preventDefault();
  514. toggleStarState(starIcon);
  515. });
  516.  
  517. return anchor;
  518. }
  519.  
  520. function toggleStarState(starIcon) {
  521. // Toggle the star state between filled and empty
  522. const storedValue = localStorage.getItem(state.vocab);
  523.  
  524. if (storedValue) {
  525. const [storedIndex, storedExactState] = storedValue.split(',');
  526. const index = parseInt(storedIndex, 10);
  527. const exactState = storedExactState === '1';
  528. if (index === state.currentExampleIndex && exactState === state.exactSearch) {
  529. localStorage.removeItem(state.vocab);
  530. starIcon.textContent = '☆';
  531. } else {
  532. localStorage.setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`);
  533. starIcon.textContent = '★';
  534. }
  535. } else {
  536. localStorage.setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`);
  537. starIcon.textContent = '★';
  538. }
  539. }
  540.  
  541. function createQuoteButton() {
  542. // Create a quote button with an icon and click event for toggling quote style
  543. const anchor = createAnchor('0rem');
  544. const quoteIcon = document.createElement('span');
  545.  
  546. // Set the icon based on exact search state
  547. quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』';
  548.  
  549. // Style the quote icon
  550. quoteIcon.style.fontSize = '1.1rem';
  551. quoteIcon.style.color = '#3D8DFF';
  552. quoteIcon.style.verticalAlign = 'middle';
  553. quoteIcon.style.position = 'relative';
  554. quoteIcon.style.top = '0px';
  555.  
  556. // Append the quote icon to the anchor and set up the click event to toggle quote state
  557. anchor.appendChild(quoteIcon);
  558. anchor.addEventListener('click', (event) => {
  559. event.preventDefault();
  560. toggleQuoteState(quoteIcon);
  561. });
  562.  
  563. return anchor;
  564. }
  565.  
  566. function toggleQuoteState(quoteIcon) {
  567. // Toggle between single and double quote styles
  568. state.exactSearch = !state.exactSearch;
  569. quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』';
  570.  
  571. // Update state based on stored data
  572. const storedData = getStoredData(state.vocab);
  573. if (storedData && storedData.exactState === state.exactSearch) {
  574. state.currentExampleIndex = storedData.index;
  575. } else {
  576. state.currentExampleIndex = 0;
  577. }
  578.  
  579. state.apiDataFetched = false;
  580. getImmersionKitData(state.vocab, state.exactSearch)
  581. .then(() => {
  582. embedImageAndPlayAudio();
  583. })
  584. .catch(error => {
  585. console.error(error);
  586. });
  587. }
  588.  
  589. function createMenuButton() {
  590. // Create a menu button with a dropdown menu
  591. const anchor = createAnchor('0.5rem');
  592. const menuIcon = document.createElement('span');
  593. menuIcon.innerHTML = '☰';
  594.  
  595. // Style the menu icon
  596. menuIcon.style.fontSize = '1.4rem';
  597. menuIcon.style.color = '#3D8DFF';
  598. menuIcon.style.verticalAlign = 'middle';
  599. menuIcon.style.position = 'relative';
  600. menuIcon.style.top = '-2px';
  601.  
  602. // Append the menu icon to the anchor and set up the click event to show the overlay menu
  603. anchor.appendChild(menuIcon);
  604. anchor.addEventListener('click', (event) => {
  605. event.preventDefault();
  606. const overlay = createOverlayMenu();
  607. document.body.appendChild(overlay);
  608. });
  609.  
  610. return anchor;
  611. }
  612.  
  613. function createTextButton(vocab, exact) {
  614. // Create a text button for the Immersion Kit
  615. const textButton = document.createElement('a');
  616. textButton.textContent = 'Immersion Kit';
  617. textButton.style.color = 'var(--subsection-label-color)';
  618. textButton.style.fontSize = '85%';
  619. textButton.style.marginRight = '0.5rem';
  620. textButton.style.verticalAlign = 'middle';
  621. textButton.href = `https://www.immersionkit.com/dictionary?keyword=${encodeURIComponent(vocab)}&sort=shortness${exact ? '&exact=true' : ''}`;
  622. textButton.target = '_blank';
  623. return textButton;
  624. }
  625.  
  626. function createButtonContainer(soundUrl, vocab, exact) {
  627. // Create a container for all buttons
  628. const buttonContainer = document.createElement('div');
  629. buttonContainer.className = 'button-container';
  630. buttonContainer.style.display = 'flex';
  631. buttonContainer.style.justifyContent = 'space-between';
  632. buttonContainer.style.alignItems = 'center';
  633. buttonContainer.style.marginBottom = '5px';
  634. buttonContainer.style.lineHeight = '1.4rem';
  635.  
  636. // Create individual buttons
  637. const menuButton = createMenuButton();
  638. const textButton = createTextButton(vocab, exact);
  639. const speakerButton = createSpeakerButton(soundUrl);
  640. const starButton = createStarButton();
  641. const quoteButton = createQuoteButton();
  642.  
  643. // Center the buttons within the container
  644. const centeredButtonsWrapper = document.createElement('div');
  645. centeredButtonsWrapper.style.display = 'flex';
  646. centeredButtonsWrapper.style.justifyContent = 'center';
  647. centeredButtonsWrapper.style.flex = '1';
  648.  
  649. centeredButtonsWrapper.append(textButton, speakerButton, starButton, quoteButton);
  650. buttonContainer.append(centeredButtonsWrapper, menuButton);
  651.  
  652. return buttonContainer;
  653. }
  654.  
  655. function stopCurrentAudio() {
  656. // Stop any currently playing audio
  657. if (state.currentAudio) {
  658. state.currentAudio.source.stop();
  659. state.currentAudio.context.close();
  660. state.currentAudio = null;
  661. }
  662. }
  663.  
  664. function playAudio(soundUrl) {
  665. // Skip playing audio if it is already playing
  666. if (state.currentlyPlayingAudio) {
  667. //console.log('Duplicate audio was skipped.');
  668. return;
  669. }
  670.  
  671. if (soundUrl) {
  672. state.currentlyPlayingAudio = true;
  673. stopCurrentAudio();
  674.  
  675. GM_xmlhttpRequest({
  676. method: 'GET',
  677. url: soundUrl,
  678. responseType: 'arraybuffer',
  679. onload: function(response) {
  680. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  681. audioContext.decodeAudioData(response.response, function(buffer) {
  682. const source = audioContext.createBufferSource();
  683. source.buffer = buffer;
  684.  
  685. const gainNode = audioContext.createGain();
  686.  
  687. // Connect the source to the gain node and the gain node to the destination
  688. source.connect(gainNode);
  689. gainNode.connect(audioContext.destination);
  690.  
  691. // Mute the first part and then ramp up the volume
  692. gainNode.gain.setValueAtTime(0, audioContext.currentTime);
  693. gainNode.gain.linearRampToValueAtTime(CONFIG.SOUND_VOLUME / 100, audioContext.currentTime + 0.1);
  694.  
  695. // Play the audio, skip the first part to avoid any "pop"
  696. source.start(0, 0.05);
  697.  
  698. // Log when the audio starts playing
  699. //console.log('Audio has started playing.');
  700.  
  701. // Save the current audio context and source for stopping later
  702. state.currentAudio = {
  703. context: audioContext,
  704. source: source
  705. };
  706.  
  707. // Set currentlyPlayingAudio to false when the audio ends
  708. source.onended = function() {
  709. state.currentlyPlayingAudio = false;
  710. };
  711. }, function(error) {
  712. console.error('Error decoding audio:', error);
  713. state.currentlyPlayingAudio = false;
  714. });
  715. },
  716. onerror: function(error) {
  717. console.error('Error fetching audio:', error);
  718. state.currentlyPlayingAudio = false;
  719. }
  720. });
  721. }
  722. }
  723.  
  724. function renderImageAndPlayAudio(vocab, shouldAutoPlaySound) {
  725. const example = state.examples[state.currentExampleIndex] || {};
  726. const imageUrl = example.image_url || null;
  727. const soundUrl = example.sound_url || null;
  728. const sentence = example.sentence || null;
  729.  
  730. // Remove any existing container
  731. removeExistingContainer();
  732. if (!shouldRenderContainer()) return;
  733.  
  734. // Create and append the main wrapper and text button container
  735. const wrapperDiv = createWrapperDiv();
  736. const textDiv = createButtonContainer(soundUrl, vocab, state.exactSearch);
  737. wrapperDiv.appendChild(textDiv);
  738.  
  739. // Handle image rendering and click event for playing audio
  740. if (state.apiDataFetched) {
  741. if (imageUrl) {
  742. const imageElement = createImageElement(wrapperDiv, imageUrl, vocab, state.exactSearch);
  743. if (imageElement) {
  744. imageElement.addEventListener('click', () => playAudio(soundUrl));
  745. }
  746. } else {
  747. const noImageText = document.createElement('div');
  748. noImageText.textContent = `NO IMAGE\n(${state.examples[state.currentExampleIndex].deck_name})`;
  749. noImageText.style.padding = '100px 0';
  750. noImageText.style.whiteSpace = 'pre'; // This ensures that newlines are respected
  751. wrapperDiv.appendChild(noImageText);
  752. }
  753. } else if (state.error) {
  754. const errorText = document.createElement('div');
  755. errorText.textContent = 'ERROR\nNO EXAMPLES FOUND\n\nRARE WORD OR\nIMMERSIONKIT API IS TEMPORARILY DOWN';
  756. errorText.style.padding = '100px 0';
  757. errorText.style.whiteSpace = 'pre'; // This ensures that newlines are respected
  758. wrapperDiv.appendChild(errorText);
  759. } else {
  760. const loadingText = document.createElement('div');
  761. loadingText.textContent = 'LOADING';
  762. loadingText.style.padding = '100px 0';
  763. wrapperDiv.appendChild(loadingText);
  764. }
  765.  
  766. // Append sentence and translation or a placeholder text
  767. sentence ? appendSentenceAndTranslation(wrapperDiv, sentence, example.translation) : appendNoneText(wrapperDiv);
  768.  
  769. // Create navigation elements
  770. const navigationDiv = createNavigationDiv();
  771. const leftArrow = createLeftArrow(vocab, shouldAutoPlaySound);
  772. const rightArrow = createRightArrow(vocab, shouldAutoPlaySound);
  773.  
  774. // Create and append the main container
  775. const containerDiv = createContainerDiv(leftArrow, wrapperDiv, rightArrow, navigationDiv);
  776. appendContainer(containerDiv);
  777.  
  778. // Auto-play sound if configured
  779. if (CONFIG.AUTO_PLAY_SOUND && shouldAutoPlaySound) {
  780. playAudio(soundUrl);
  781. }
  782. }
  783.  
  784. function removeExistingContainer() {
  785. // Remove the existing container if it exists
  786. const existingContainer = document.getElementById('immersion-kit-container');
  787. if (existingContainer) {
  788. existingContainer.remove();
  789. }
  790. }
  791.  
  792. function shouldRenderContainer() {
  793. // Determine if the container should be rendered based on the presence of certain elements
  794. const resultVocabularySection = document.querySelector('.result.vocabulary');
  795. const hboxWrapSection = document.querySelector('.hbox.wrap');
  796. const subsectionMeanings = document.querySelector('.subsection-meanings');
  797. const subsectionLabels = document.querySelectorAll('h6.subsection-label');
  798. return resultVocabularySection || hboxWrapSection || subsectionMeanings || subsectionLabels.length >= 3;
  799. }
  800.  
  801. function createWrapperDiv() {
  802. // Create and style the wrapper div
  803. const wrapperDiv = document.createElement('div');
  804. wrapperDiv.id = 'image-wrapper';
  805. wrapperDiv.style.textAlign = 'center';
  806. wrapperDiv.style.padding = '5px 0';
  807. return wrapperDiv;
  808. }
  809.  
  810. function createImageElement(wrapperDiv, imageUrl, vocab, exactSearch) {
  811. // Create and return an image element with specified attributes
  812. const searchVocab = exactSearch ? `「${vocab}」` : vocab;
  813. const titleText = `${searchVocab} #${state.currentExampleIndex + 1} \n${state.examples[state.currentExampleIndex].deck_name}`;
  814. return GM_addElement(wrapperDiv, 'img', {
  815. src: imageUrl,
  816. alt: 'Embedded Image',
  817. title: titleText,
  818. style: `max-width: ${CONFIG.IMAGE_WIDTH}; margin-top: 10px; cursor: pointer;`
  819. });
  820. }
  821.  
  822. function highlightVocab(sentence, vocab) {
  823. // Highlight vocabulary in the sentence based on configuration
  824. if (!CONFIG.COLORED_SENTENCE_TEXT) return sentence;
  825.  
  826. if (state.exactSearch) {
  827. const regex = new RegExp(`(${vocab})`, 'g');
  828. return sentence.replace(regex, '<span style="color: var(--outline-input-color);">$1</span>');
  829. } else {
  830. return vocab.split('').reduce((acc, char) => {
  831. const regex = new RegExp(char, 'g');
  832. return acc.replace(regex, `<span style="color: var(--outline-input-color);">${char}</span>`);
  833. }, sentence);
  834. }
  835. }
  836.  
  837. function appendSentenceAndTranslation(wrapperDiv, sentence, translation) {
  838. // Append sentence and translation to the wrapper div
  839. const sentenceText = document.createElement('div');
  840. sentenceText.innerHTML = highlightVocab(sentence, state.vocab);
  841. sentenceText.style.marginTop = '10px';
  842. sentenceText.style.fontSize = CONFIG.SENTENCE_FONT_SIZE;
  843. sentenceText.style.color = 'lightgray';
  844. sentenceText.style.maxWidth = CONFIG.IMAGE_WIDTH;
  845. sentenceText.style.whiteSpace = 'pre-wrap';
  846. wrapperDiv.appendChild(sentenceText);
  847.  
  848. if (CONFIG.ENABLE_EXAMPLE_TRANSLATION && translation) {
  849. const translationText = document.createElement('div');
  850. translationText.innerHTML = replaceSpecialCharacters(translation);
  851. translationText.style.marginTop = '5px';
  852. translationText.style.fontSize = CONFIG.TRANSLATION_FONT_SIZE;
  853. translationText.style.color = 'var(--subsection-label-color)';
  854. translationText.style.maxWidth = CONFIG.IMAGE_WIDTH;
  855. translationText.style.whiteSpace = 'pre-wrap';
  856. wrapperDiv.appendChild(translationText);
  857. }
  858. }
  859.  
  860. function appendNoneText(wrapperDiv) {
  861. // Append a "None" text to the wrapper div
  862. const noneText = document.createElement('div');
  863. noneText.textContent = 'None';
  864. noneText.style.marginTop = '10px';
  865. noneText.style.fontSize = '85%';
  866. noneText.style.color = 'var(--subsection-label-color)';
  867. wrapperDiv.appendChild(noneText);
  868. }
  869.  
  870. function createNavigationDiv() {
  871. // Create and style the navigation div
  872. const navigationDiv = document.createElement('div');
  873. navigationDiv.id = 'immersion-kit-embed';
  874. navigationDiv.style.display = 'flex';
  875. navigationDiv.style.justifyContent = 'center';
  876. navigationDiv.style.alignItems = 'center';
  877. navigationDiv.style.maxWidth = CONFIG.IMAGE_WIDTH;
  878. navigationDiv.style.margin = '0 auto';
  879. return navigationDiv;
  880. }
  881.  
  882. function createLeftArrow(vocab, shouldAutoPlaySound) {
  883. // Create and configure the left arrow button
  884. const leftArrow = document.createElement('button');
  885. leftArrow.textContent = '<';
  886. leftArrow.style.marginRight = '10px';
  887. leftArrow.disabled = state.currentExampleIndex === 0;
  888. leftArrow.addEventListener('click', () => {
  889. if (state.currentExampleIndex > 0) {
  890. state.currentExampleIndex--;
  891. state.currentlyPlayingAudio = false;
  892. renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
  893. preloadImages();
  894. }
  895. });
  896. return leftArrow;
  897. }
  898.  
  899. function createRightArrow(vocab, shouldAutoPlaySound) {
  900. // Create and configure the right arrow button
  901. const rightArrow = document.createElement('button');
  902. rightArrow.textContent = '>';
  903. rightArrow.style.marginLeft = '10px';
  904. rightArrow.disabled = state.currentExampleIndex >= state.examples.length - 1;
  905. rightArrow.addEventListener('click', () => {
  906. if (state.currentExampleIndex < state.examples.length - 1) {
  907. state.currentExampleIndex++;
  908. state.currentlyPlayingAudio = false;
  909. renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
  910. preloadImages();
  911. }
  912. });
  913. return rightArrow;
  914. }
  915.  
  916. function createContainerDiv(leftArrow, wrapperDiv, rightArrow, navigationDiv) {
  917. // Create and configure the main container div
  918. const containerDiv = document.createElement('div');
  919. containerDiv.id = 'immersion-kit-container';
  920. containerDiv.style.display = 'flex';
  921. containerDiv.style.alignItems = 'center';
  922. containerDiv.style.justifyContent = 'center';
  923. containerDiv.style.flexDirection = 'column';
  924.  
  925. const arrowWrapperDiv = document.createElement('div');
  926. arrowWrapperDiv.style.display = 'flex';
  927. arrowWrapperDiv.style.alignItems = 'center';
  928. arrowWrapperDiv.style.justifyContent = 'center';
  929.  
  930. arrowWrapperDiv.append(leftArrow, wrapperDiv, rightArrow);
  931. containerDiv.append(arrowWrapperDiv, navigationDiv);
  932.  
  933. return containerDiv;
  934. }
  935.  
  936. function appendContainer(containerDiv) {
  937. // Append the container div to the appropriate section based on configuration
  938. const resultVocabularySection = document.querySelector('.result.vocabulary');
  939. const hboxWrapSection = document.querySelector('.hbox.wrap');
  940. const subsectionMeanings = document.querySelector('.subsection-meanings');
  941. const subsectionComposedOfKanji = document.querySelector('.subsection-composed-of-kanji');
  942. const subsectionPitchAccent = document.querySelector('.subsection-pitch-accent');
  943. const subsectionLabels = document.querySelectorAll('h6.subsection-label');
  944. const vboxGap = document.querySelector('.vbox.gap');
  945.  
  946. if (CONFIG.WIDE_MODE && subsectionMeanings) {
  947. const wrapper = document.createElement('div');
  948. wrapper.style.display = 'flex';
  949. wrapper.style.alignItems = 'flex-start';
  950.  
  951. const originalContentWrapper = document.createElement('div');
  952. originalContentWrapper.style.flex = '1';
  953. originalContentWrapper.appendChild(subsectionMeanings);
  954.  
  955. if (subsectionComposedOfKanji) {
  956. const newline1 = document.createElement('br');
  957. originalContentWrapper.appendChild(newline1);
  958. originalContentWrapper.appendChild(subsectionComposedOfKanji);
  959. }
  960. if (subsectionPitchAccent) {
  961. const newline2 = document.createElement('br');
  962. originalContentWrapper.appendChild(newline2);
  963. originalContentWrapper.appendChild(subsectionPitchAccent);
  964. }
  965.  
  966. wrapper.appendChild(originalContentWrapper);
  967. wrapper.appendChild(containerDiv);
  968.  
  969. if (vboxGap) {
  970. const existingDynamicDiv = vboxGap.querySelector('#dynamic-content');
  971. if (existingDynamicDiv) {
  972. existingDynamicDiv.remove();
  973. }
  974.  
  975. const dynamicDiv = document.createElement('div');
  976. dynamicDiv.id = 'dynamic-content';
  977. dynamicDiv.appendChild(wrapper);
  978.  
  979. if (window.location.href.includes('vocabulary')) {
  980. vboxGap.insertBefore(dynamicDiv, vboxGap.children[1]);
  981. } else {
  982. vboxGap.insertBefore(dynamicDiv, vboxGap.firstChild);
  983. }
  984. }
  985. } else {
  986. if (state.embedAboveSubsectionMeanings && subsectionMeanings) {
  987. subsectionMeanings.parentNode.insertBefore(containerDiv, subsectionMeanings);
  988. } else if (resultVocabularySection) {
  989. resultVocabularySection.parentNode.insertBefore(containerDiv, resultVocabularySection);
  990. } else if (hboxWrapSection) {
  991. hboxWrapSection.parentNode.insertBefore(containerDiv, hboxWrapSection);
  992. } else if (subsectionLabels.length >= 4) {
  993. subsectionLabels[3].parentNode.insertBefore(containerDiv, subsectionLabels[3]);
  994. }
  995. }
  996. }
  997.  
  998. function embedImageAndPlayAudio() {
  999. // Embed the image and play audio, removing existing navigation div if present
  1000. const existingNavigationDiv = document.getElementById('immersion-kit-embed');
  1001. if (existingNavigationDiv) existingNavigationDiv.remove();
  1002.  
  1003. const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
  1004.  
  1005. renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
  1006. preloadImages();
  1007. }
  1008.  
  1009. function replaceSpecialCharacters(text) {
  1010. // Replace special characters in the text
  1011. return text.replace(/<br>/g, '\n').replace(/&quot;/g, '"').replace(/\n/g, '<br>');
  1012. }
  1013.  
  1014. function preloadImages() {
  1015. // Preload images around the current example index
  1016. const preloadDiv = GM_addElement(document.body, 'div', { style: 'display: none;' });
  1017. const startIndex = Math.max(0, state.currentExampleIndex - CONFIG.NUMBER_OF_PRELOADS);
  1018. const endIndex = Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUMBER_OF_PRELOADS);
  1019.  
  1020. for (let i = startIndex; i <= endIndex; i++) {
  1021. if (!state.preloadedIndices.has(i) && state.examples[i].image_url) {
  1022. GM_addElement(preloadDiv, 'img', { src: state.examples[i].image_url });
  1023. state.preloadedIndices.add(i);
  1024. }
  1025. }
  1026. }
  1027.  
  1028.  
  1029. //MENU FUNCTIONS=====================================================================================================================
  1030. ////FILE OPERATIONS=====================================================================================================================
  1031. function handleImportButtonClick() {
  1032. handleFileInput('application/json', importFavorites);
  1033. }
  1034.  
  1035. function handleFileInput(acceptType, callback) {
  1036. const fileInput = document.createElement('input');
  1037. fileInput.type = 'file';
  1038. fileInput.accept = acceptType;
  1039. fileInput.addEventListener('change', callback);
  1040. fileInput.click();
  1041. }
  1042.  
  1043. function createBlobAndDownload(data, filename, type) {
  1044. const blob = new Blob([data], { type });
  1045. const url = URL.createObjectURL(blob);
  1046. const a = document.createElement('a');
  1047. a.href = url;
  1048. a.download = filename;
  1049. document.body.appendChild(a);
  1050. a.click();
  1051. document.body.removeChild(a);
  1052. URL.revokeObjectURL(url);
  1053. }
  1054.  
  1055. function exportFavorites() {
  1056. const favorites = {};
  1057. for (let i = 0; i < localStorage.length; i++) {
  1058. const key = localStorage.key(i);
  1059. if (!key.startsWith('CONFIG')) {
  1060. favorites[key] = localStorage.getItem(key);
  1061. }
  1062. }
  1063. const data = JSON.stringify(favorites, null, 2);
  1064. createBlobAndDownload(data, 'favorites.json', 'application/json');
  1065. }
  1066.  
  1067. function importFavorites(event) {
  1068. const file = event.target.files[0];
  1069. if (!file) return;
  1070.  
  1071. const reader = new FileReader();
  1072. reader.onload = function(e) {
  1073. try {
  1074. const favorites = JSON.parse(e.target.result);
  1075. for (const key in favorites) {
  1076. localStorage.setItem(key, favorites[key]);
  1077. }
  1078. alert('Favorites imported successfully!');
  1079. location.reload();
  1080. } catch (error) {
  1081. alert('Error importing favorites:', error);
  1082. }
  1083. };
  1084. reader.readAsText(file);
  1085. }
  1086.  
  1087. ////CONFIRMATION
  1088. function createConfirmationPopup(messageText, onYes, onNo) {
  1089. // Create a confirmation popup with Yes and No buttons
  1090. const popupOverlay = document.createElement('div');
  1091. popupOverlay.style.position = 'fixed';
  1092. popupOverlay.style.top = '0';
  1093. popupOverlay.style.left = '0';
  1094. popupOverlay.style.width = '100%';
  1095. popupOverlay.style.height = '100%';
  1096. popupOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
  1097. popupOverlay.style.zIndex = '1001';
  1098. popupOverlay.style.display = 'flex';
  1099. popupOverlay.style.justifyContent = 'center';
  1100. popupOverlay.style.alignItems = 'center';
  1101.  
  1102. const popupContent = document.createElement('div');
  1103. popupContent.style.backgroundColor = 'var(--background-color)';
  1104. popupContent.style.padding = '20px';
  1105. popupContent.style.borderRadius = '5px';
  1106. popupContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
  1107. popupContent.style.textAlign = 'center';
  1108.  
  1109. const message = document.createElement('p');
  1110. message.textContent = messageText;
  1111.  
  1112. const yesButton = document.createElement('button');
  1113. yesButton.textContent = 'Yes';
  1114. yesButton.style.backgroundColor = '#C82800';
  1115. yesButton.style.marginRight = '10px';
  1116. yesButton.addEventListener('click', () => {
  1117. onYes();
  1118. document.body.removeChild(popupOverlay);
  1119. });
  1120.  
  1121. const noButton = document.createElement('button');
  1122. noButton.textContent = 'No';
  1123. noButton.addEventListener('click', () => {
  1124. onNo();
  1125. document.body.removeChild(popupOverlay);
  1126. });
  1127.  
  1128. popupContent.appendChild(message);
  1129. popupContent.appendChild(yesButton);
  1130. popupContent.appendChild(noButton);
  1131. popupOverlay.appendChild(popupContent);
  1132.  
  1133. document.body.appendChild(popupOverlay);
  1134. }
  1135.  
  1136. ////BUTTONS
  1137. function createActionButtonsContainer() {
  1138. const actionButtonWidth = '100px';
  1139.  
  1140. const closeButton = createButton('Close', '10px', closeOverlayMenu, actionButtonWidth);
  1141. const saveButton = createButton('Save', '10px', saveConfig, actionButtonWidth);
  1142. const defaultButton = createDefaultButton(actionButtonWidth);
  1143. const deleteButton = createDeleteButton(actionButtonWidth);
  1144.  
  1145. const actionButtonsContainer = document.createElement('div');
  1146. actionButtonsContainer.style.textAlign = 'center';
  1147. actionButtonsContainer.style.marginTop = '10px';
  1148. actionButtonsContainer.append(closeButton, saveButton, defaultButton, deleteButton);
  1149.  
  1150. return actionButtonsContainer;
  1151. }
  1152.  
  1153. function createMenuButtons() {
  1154. const exportImportContainer = createExportImportContainer();
  1155. const actionButtonsContainer = createActionButtonsContainer();
  1156.  
  1157. const buttonContainer = document.createElement('div');
  1158. buttonContainer.append(exportImportContainer, actionButtonsContainer);
  1159.  
  1160. return buttonContainer;
  1161. }
  1162.  
  1163. function createButton(text, margin, onClick, width) {
  1164. // Create a button element with specified properties
  1165. const button = document.createElement('button');
  1166. button.textContent = text;
  1167. button.style.margin = margin;
  1168. button.style.width = width;
  1169. button.style.textAlign = 'center';
  1170. button.style.display = 'inline-block';
  1171. button.style.lineHeight = '30px';
  1172. button.style.padding = '5px 0';
  1173. button.addEventListener('click', onClick);
  1174. return button;
  1175. }
  1176. ////IMPORT/EXPORT BUTTONS
  1177. function createExportImportContainer() {
  1178. const exportImportButtonWidth = '200px';
  1179.  
  1180. const exportButton = createButton('Export Favorites', '10px', exportFavorites, exportImportButtonWidth);
  1181. const importButton = createButton('Import Favorites', '10px', handleImportButtonClick, exportImportButtonWidth);
  1182.  
  1183. const exportImportContainer = document.createElement('div');
  1184. exportImportContainer.style.textAlign = 'center';
  1185. exportImportContainer.style.marginTop = '10px';
  1186. exportImportContainer.append(exportButton, importButton);
  1187.  
  1188. return exportImportContainer;
  1189. }
  1190. ////CLOSE BUTTON
  1191. function closeOverlayMenu() {
  1192. loadConfig();
  1193. document.body.removeChild(document.getElementById('overlayMenu'));
  1194. }
  1195.  
  1196. ////SAVE BUTTON
  1197. function saveConfig() {
  1198. const overlay = document.getElementById('overlayMenu');
  1199. if (!overlay) return;
  1200.  
  1201. const inputs = overlay.querySelectorAll('input, span');
  1202. const { changes, minimumExampleLengthChanged, newMinimumExampleLength } = gatherChanges(inputs);
  1203.  
  1204. if (minimumExampleLengthChanged) {
  1205. handleMinimumExampleLengthChange(newMinimumExampleLength, changes);
  1206. } else {
  1207. applyChanges(changes);
  1208. finalizeSaveConfig();
  1209. setVocabSize();
  1210. setPageWidth();
  1211. }
  1212. }
  1213.  
  1214. function gatherChanges(inputs) {
  1215. let minimumExampleLengthChanged = false;
  1216. let newMinimumExampleLength;
  1217. const changes = {};
  1218.  
  1219. inputs.forEach(input => {
  1220. const key = input.getAttribute('data-key');
  1221. const type = input.getAttribute('data-type');
  1222. let value;
  1223.  
  1224. if (type === 'boolean') {
  1225. value = input.checked;
  1226. } else if (type === 'number') {
  1227. value = parseFloat(input.textContent);
  1228. } else if (type === 'string') {
  1229. value = input.textContent;
  1230. }
  1231.  
  1232. if (key && type) {
  1233. const typePart = input.getAttribute('data-type-part');
  1234. const originalFormattedType = typePart.slice(1, -1);
  1235.  
  1236. if (key === 'MINIMUM_EXAMPLE_LENGTH' && CONFIG.MINIMUM_EXAMPLE_LENGTH !== value) {
  1237. minimumExampleLengthChanged = true;
  1238. newMinimumExampleLength = value;
  1239. }
  1240.  
  1241. changes[`CONFIG.${key}`] = value + originalFormattedType;
  1242. }
  1243. });
  1244.  
  1245. return { changes, minimumExampleLengthChanged, newMinimumExampleLength };
  1246. }
  1247.  
  1248. function handleMinimumExampleLengthChange(newMinimumExampleLength, changes) {
  1249. createConfirmationPopup(
  1250. 'Changing Minimum Example Length will break your current favorites. They will all be deleted. Are you sure?',
  1251. async () => {
  1252. await IndexedDBManager.delete();
  1253. CONFIG.MINIMUM_EXAMPLE_LENGTH = newMinimumExampleLength;
  1254. localStorage.setItem('CONFIG.MINIMUM_EXAMPLE_LENGTH', newMinimumExampleLength);
  1255. applyChanges(changes);
  1256. clearNonConfigLocalStorage();
  1257. finalizeSaveConfig();
  1258. location.reload();
  1259. },
  1260. () => {
  1261. const overlay = document.getElementById('overlayMenu');
  1262. document.body.removeChild(overlay);
  1263. document.body.appendChild(createOverlayMenu());
  1264. }
  1265. );
  1266. }
  1267.  
  1268. function clearNonConfigLocalStorage() {
  1269. for (let i = 0; i < localStorage.length; i++) {
  1270. const key = localStorage.key(i);
  1271. if (key && !key.startsWith('CONFIG')) {
  1272. localStorage.removeItem(key);
  1273. i--; // Adjust index after removal
  1274. }
  1275. }
  1276. }
  1277.  
  1278. function applyChanges(changes) {
  1279. for (const key in changes) {
  1280. localStorage.setItem(key, changes[key]);
  1281. }
  1282. }
  1283.  
  1284. function finalizeSaveConfig() {
  1285. loadConfig();
  1286. renderImageAndPlayAudio(state.vocab, CONFIG.AUTO_PLAY_SOUND);
  1287. const overlay = document.getElementById('overlayMenu');
  1288. if (overlay) {
  1289. document.body.removeChild(overlay);
  1290. }
  1291. }
  1292.  
  1293.  
  1294. ////DEFAULT BUTTON
  1295. function createDefaultButton(width) {
  1296. const defaultButton = createButton('Default', '10px', () => {
  1297. createConfirmationPopup(
  1298. 'This will reset all your settings to default. Are you sure?',
  1299. () => {
  1300. Object.keys(localStorage).forEach(key => {
  1301. if (key.startsWith('CONFIG')) {
  1302. localStorage.removeItem(key);
  1303. }
  1304. });
  1305. location.reload();
  1306. },
  1307. () => {
  1308. const overlay = document.getElementById('overlayMenu');
  1309. if (overlay) {
  1310. document.body.removeChild(overlay);
  1311. }
  1312. loadConfig();
  1313. document.body.appendChild(createOverlayMenu());
  1314. }
  1315. );
  1316. }, width);
  1317. defaultButton.style.backgroundColor = '#C82800';
  1318. defaultButton.style.color = 'white';
  1319. return defaultButton;
  1320. }
  1321.  
  1322.  
  1323. ////DELETE BUTTON
  1324. function createDeleteButton(width) {
  1325. const deleteButton = createButton('DELETE', '10px', () => {
  1326. createConfirmationPopup(
  1327. 'This will delete all your favorites and cached data. Are you sure?',
  1328. async () => {
  1329. await IndexedDBManager.delete();
  1330. Object.keys(localStorage).forEach(key => {
  1331. if (!key.startsWith('CONFIG')) {
  1332. localStorage.removeItem(key);
  1333. }
  1334. });
  1335. location.reload();
  1336. },
  1337. () => {
  1338. const overlay = document.getElementById('overlayMenu');
  1339. if (overlay) {
  1340. document.body.removeChild(overlay);
  1341. }
  1342. loadConfig();
  1343. document.body.appendChild(createOverlayMenu());
  1344. }
  1345. );
  1346. }, width);
  1347. deleteButton.style.backgroundColor = '#C82800';
  1348. deleteButton.style.color = 'white';
  1349. return deleteButton;
  1350. }
  1351.  
  1352. function createOverlayMenu() {
  1353. // Create and return the overlay menu for configuration settings
  1354. const overlay = document.createElement('div');
  1355. overlay.id = 'overlayMenu';
  1356. overlay.style.position = 'fixed';
  1357. overlay.style.top = '0';
  1358. overlay.style.left = '0';
  1359. overlay.style.width = '100%';
  1360. overlay.style.height = '100%';
  1361. overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
  1362. overlay.style.zIndex = '1000';
  1363. overlay.style.display = 'flex';
  1364. overlay.style.justifyContent = 'center';
  1365. overlay.style.alignItems = 'center';
  1366.  
  1367. const menuContent = document.createElement('div');
  1368. menuContent.style.backgroundColor = 'var(--background-color)';
  1369. menuContent.style.color = 'var(--text-color)';
  1370. menuContent.style.padding = '20px';
  1371. menuContent.style.borderRadius = '5px';
  1372. menuContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
  1373. menuContent.style.width = '80%';
  1374. menuContent.style.maxWidth = '550px';
  1375. menuContent.style.maxHeight = '80%';
  1376. menuContent.style.overflowY = 'auto';
  1377.  
  1378. for (const [key, value] of Object.entries(CONFIG)) {
  1379. const optionContainer = document.createElement('div');
  1380. optionContainer.style.marginBottom = '10px';
  1381. optionContainer.style.display = 'flex';
  1382. optionContainer.style.alignItems = 'center';
  1383.  
  1384. const leftContainer = document.createElement('div');
  1385. leftContainer.style.flex = '1';
  1386. leftContainer.style.display = 'flex';
  1387. leftContainer.style.alignItems = 'center';
  1388.  
  1389. const rightContainer = document.createElement('div');
  1390. rightContainer.style.flex = '1';
  1391. rightContainer.style.display = 'flex';
  1392. rightContainer.style.alignItems = 'center';
  1393. rightContainer.style.justifyContent = 'center';
  1394.  
  1395. const label = document.createElement('label');
  1396. label.textContent = key.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
  1397. label.style.marginRight = '10px';
  1398.  
  1399. leftContainer.appendChild(label);
  1400.  
  1401. if (typeof value === 'boolean') {
  1402. const checkboxContainer = document.createElement('div');
  1403. checkboxContainer.style.display = 'flex';
  1404. checkboxContainer.style.alignItems = 'center';
  1405. checkboxContainer.style.justifyContent = 'center';
  1406.  
  1407. const checkbox = document.createElement('input');
  1408. checkbox.type = 'checkbox';
  1409. checkbox.checked = value;
  1410. checkbox.setAttribute('data-key', key);
  1411. checkbox.setAttribute('data-type', 'boolean');
  1412. checkbox.setAttribute('data-type-part', '');
  1413. checkboxContainer.appendChild(checkbox);
  1414.  
  1415. rightContainer.appendChild(checkboxContainer);
  1416. } else if (typeof value === 'number') {
  1417. const numberContainer = document.createElement('div');
  1418. numberContainer.style.display = 'flex';
  1419. numberContainer.style.alignItems = 'center';
  1420. numberContainer.style.justifyContent = 'center';
  1421.  
  1422. const decrementButton = document.createElement('button');
  1423. decrementButton.textContent = '-';
  1424. decrementButton.style.marginRight = '5px';
  1425.  
  1426. const input = document.createElement('span');
  1427. input.textContent = value;
  1428. input.style.margin = '0 10px';
  1429. input.style.minWidth = '3ch';
  1430. input.style.textAlign = 'center';
  1431. input.setAttribute('data-key', key);
  1432. input.setAttribute('data-type', 'number');
  1433. input.setAttribute('data-type-part', '');
  1434.  
  1435. const incrementButton = document.createElement('button');
  1436. incrementButton.textContent = '+';
  1437. incrementButton.style.marginLeft = '5px';
  1438.  
  1439. const updateButtonStates = () => {
  1440. let currentValue = parseFloat(input.textContent);
  1441. if (currentValue <= 0) {
  1442. decrementButton.disabled = true;
  1443. decrementButton.style.color = 'grey';
  1444. } else {
  1445. decrementButton.disabled = false;
  1446. decrementButton.style.color = '';
  1447. }
  1448. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  1449. incrementButton.disabled = true;
  1450. incrementButton.style.color = 'grey';
  1451. } else {
  1452. incrementButton.disabled = false;
  1453. incrementButton.style.color = '';
  1454. }
  1455. };
  1456.  
  1457. decrementButton.addEventListener('click', () => {
  1458. let currentValue = parseFloat(input.textContent);
  1459. if (currentValue > 0) {
  1460. if (currentValue > 200) {
  1461. input.textContent = currentValue - 25;
  1462. } else if (currentValue > 20) {
  1463. input.textContent = currentValue - 5;
  1464. } else {
  1465. input.textContent = currentValue - 1;
  1466. }
  1467. updateButtonStates();
  1468. }
  1469. });
  1470.  
  1471. incrementButton.addEventListener('click', () => {
  1472. let currentValue = parseFloat(input.textContent);
  1473. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  1474. return;
  1475. }
  1476. if (currentValue >= 200) {
  1477. input.textContent = currentValue + 25;
  1478. } else if (currentValue >= 20) {
  1479. input.textContent = currentValue + 5;
  1480. } else {
  1481. input.textContent = currentValue + 1;
  1482. }
  1483. updateButtonStates();
  1484. });
  1485.  
  1486. numberContainer.appendChild(decrementButton);
  1487. numberContainer.appendChild(input);
  1488. numberContainer.appendChild(incrementButton);
  1489.  
  1490. rightContainer.appendChild(numberContainer);
  1491.  
  1492. // Initialize button states
  1493. updateButtonStates();
  1494. } else if (typeof value === 'string') {
  1495. const typeParts = value.split(/(\d+)/).filter(Boolean);
  1496. const numberParts = typeParts.filter(part => !isNaN(part)).map(Number);
  1497.  
  1498. const numberContainer = document.createElement('div');
  1499. numberContainer.style.display = 'flex';
  1500. numberContainer.style.alignItems = 'center';
  1501. numberContainer.style.justifyContent = 'center';
  1502.  
  1503. const typeSpan = document.createElement('span');
  1504. const formattedType = '(' + typeParts.filter(part => isNaN(part)).join('').replace(/_/g, ' ').toLowerCase() + ')';
  1505. typeSpan.textContent = formattedType;
  1506. typeSpan.style.marginRight = '10px';
  1507.  
  1508. leftContainer.appendChild(typeSpan);
  1509.  
  1510. typeParts.forEach(part => {
  1511. if (!isNaN(part)) {
  1512. const decrementButton = document.createElement('button');
  1513. decrementButton.textContent = '-';
  1514. decrementButton.style.marginRight = '5px';
  1515.  
  1516. const input = document.createElement('span');
  1517. input.textContent = part;
  1518. input.style.margin = '0 10px';
  1519. input.style.minWidth = '3ch';
  1520. input.style.textAlign = 'center';
  1521. input.setAttribute('data-key', key);
  1522. input.setAttribute('data-type', 'string');
  1523. input.setAttribute('data-type-part', formattedType);
  1524.  
  1525. const incrementButton = document.createElement('button');
  1526. incrementButton.textContent = '+';
  1527. incrementButton.style.marginLeft = '5px';
  1528.  
  1529. const updateButtonStates = () => {
  1530. let currentValue = parseFloat(input.textContent);
  1531. if (currentValue <= 0) {
  1532. decrementButton.disabled = true;
  1533. decrementButton.style.color = 'grey';
  1534. } else {
  1535. decrementButton.disabled = false;
  1536. decrementButton.style.color = '';
  1537. }
  1538. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  1539. incrementButton.disabled = true;
  1540. incrementButton.style.color = 'grey';
  1541. } else {
  1542. incrementButton.disabled = false;
  1543. incrementButton.style.color = '';
  1544. }
  1545. };
  1546.  
  1547. decrementButton.addEventListener('click', () => {
  1548. let currentValue = parseFloat(input.textContent);
  1549. if (currentValue > 0) {
  1550. if (currentValue > 200) {
  1551. input.textContent = currentValue - 25;
  1552. } else if (currentValue > 20) {
  1553. input.textContent = currentValue - 5;
  1554. } else {
  1555. input.textContent = currentValue - 1;
  1556. }
  1557. updateButtonStates();
  1558. }
  1559. });
  1560.  
  1561. incrementButton.addEventListener('click', () => {
  1562. let currentValue = parseFloat(input.textContent);
  1563. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  1564. return;
  1565. }
  1566. if (currentValue >= 200) {
  1567. input.textContent = currentValue + 25;
  1568. } else if (currentValue >= 20) {
  1569. input.textContent = currentValue + 5;
  1570. } else {
  1571. input.textContent = currentValue + 1;
  1572. }
  1573. updateButtonStates();
  1574. });
  1575.  
  1576. numberContainer.appendChild(decrementButton);
  1577. numberContainer.appendChild(input);
  1578. numberContainer.appendChild(incrementButton);
  1579.  
  1580. // Initialize button states
  1581. updateButtonStates();
  1582. }
  1583. });
  1584.  
  1585. rightContainer.appendChild(numberContainer);
  1586. }
  1587.  
  1588. optionContainer.appendChild(leftContainer);
  1589. optionContainer.appendChild(rightContainer);
  1590. menuContent.appendChild(optionContainer);
  1591. }
  1592.  
  1593. const menuButtons = createMenuButtons();
  1594. menuContent.appendChild(menuButtons);
  1595.  
  1596. overlay.appendChild(menuContent);
  1597.  
  1598. return overlay;
  1599. }
  1600.  
  1601. function loadConfig() {
  1602. for (const key in localStorage) {
  1603. if (!localStorage.hasOwnProperty(key) || !key.startsWith('CONFIG.')) continue;
  1604.  
  1605. const configKey = key.substring('CONFIG.'.length);
  1606. if (!CONFIG.hasOwnProperty(configKey)) continue;
  1607.  
  1608. const savedValue = localStorage.getItem(key);
  1609. if (savedValue === null) continue;
  1610.  
  1611. const valueType = typeof CONFIG[configKey];
  1612. if (valueType === 'boolean') {
  1613. CONFIG[configKey] = savedValue === 'true';
  1614. } else if (valueType === 'number') {
  1615. CONFIG[configKey] = parseFloat(savedValue);
  1616. } else if (valueType === 'string') {
  1617. CONFIG[configKey] = savedValue;
  1618. }
  1619. }
  1620. }
  1621.  
  1622.  
  1623. //MAIN FUNCTIONS=====================================================================================================================
  1624. function onPageLoad() {
  1625. // Initialize state and determine vocabulary based on URL
  1626. state.embedAboveSubsectionMeanings = false;
  1627.  
  1628. const url = window.location.href;
  1629. const machineTranslationFrame = document.getElementById('machine-translation-frame');
  1630.  
  1631. // Proceed only if the machine translation frame is not present
  1632. if (!machineTranslationFrame) {
  1633.  
  1634. //display embed for first time with loading text
  1635. embedImageAndPlayAudio();
  1636. setPageWidth();
  1637.  
  1638. if (url.includes('/vocabulary/')) {
  1639. state.vocab = parseVocabFromVocabulary();
  1640. } else if (url.includes('/search?q=')) {
  1641. state.vocab = parseVocabFromSearch();
  1642. } else if (url.includes('c=')) {
  1643. state.vocab = parseVocabFromAnswer();
  1644. } else if (url.includes('/kanji/')) {
  1645. state.vocab = parseVocabFromKanji();
  1646. } else {
  1647. state.vocab = parseVocabFromReview();
  1648. }
  1649. } else {
  1650. console.log('Machine translation frame detected, skipping vocabulary parsing.');
  1651. }
  1652.  
  1653. // Retrieve stored data for the current vocabulary
  1654. const { index, exactState } = getStoredData(state.vocab);
  1655. state.currentExampleIndex = index;
  1656. state.exactSearch = exactState;
  1657.  
  1658. // Fetch data and embed image/audio if necessary
  1659. if (state.vocab && !state.apiDataFetched) {
  1660. getImmersionKitData(state.vocab, state.exactSearch)
  1661. .then(() => {
  1662. preloadImages();
  1663. if (!/https:\/\/jpdb\.io\/review(#a)?$/.test(url)) {
  1664. embedImageAndPlayAudio();
  1665. }
  1666. })
  1667. .catch(console.error);
  1668. } else if (state.apiDataFetched) {
  1669. embedImageAndPlayAudio();
  1670. //preloadImages();
  1671. setVocabSize();
  1672. setPageWidth();
  1673. }
  1674. }
  1675.  
  1676. function setPageWidth() {
  1677. // Set the maximum width of the page
  1678. document.body.style.maxWidth = CONFIG.PAGE_WIDTH;
  1679. }
  1680.  
  1681. // Observe URL changes and reload the page content accordingly
  1682. const observer = new MutationObserver(() => {
  1683. if (window.location.href !== observer.lastUrl) {
  1684. observer.lastUrl = window.location.href;
  1685. onPageLoad();
  1686. }
  1687. });
  1688.  
  1689. // Function to apply styles
  1690. function setVocabSize() {
  1691. // Create a new style element
  1692. const style = document.createElement('style');
  1693. style.type = 'text/css';
  1694. style.innerHTML = `
  1695. .answer-box > .plain {
  1696. font-size: ${CONFIG.VOCAB_SIZE} !important; /* Use the configurable font size */
  1697. padding-bottom: 0.1rem !important; /* Retain padding */
  1698. }
  1699. `;
  1700.  
  1701. // Append the new style to the document head
  1702. document.head.appendChild(style);
  1703. }
  1704. observer.lastUrl = window.location.href;
  1705. observer.observe(document, { subtree: true, childList: true });
  1706.  
  1707. // Add event listeners for page load and URL changes
  1708. window.addEventListener('load', onPageLoad);
  1709. window.addEventListener('popstate', onPageLoad);
  1710. window.addEventListener('hashchange', onPageLoad);
  1711.  
  1712. // Initial configuration and preloading
  1713. loadConfig();
  1714. setPageWidth();
  1715. setVocabSize();
  1716. //preloadImages();
  1717.  
  1718. })();