JPDB Immersion Kit Examples

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

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