JPDB Immersion Kit Examples

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

目前為 2025-03-13 提交的版本,檢視 最新版本

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