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

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