Greasy Fork 还支持 简体中文。

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.4
  4. // @description Embeds anime images & audio examples into JPDB review and vocabulary pages using Immersion Kit's API. Compatible only with TamperMonkey.
  5. // @author awoo
  6. // @namespace jpdb-immersion-kit-examples
  7. // @match https://jpdb.io/review*
  8. // @match https://jpdb.io/vocabulary/*
  9. // @match https://jpdb.io/kanji/*
  10. // @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. const styleSheet = document.querySelector('link[rel="stylesheet"]').sheet;
  1123.  
  1124. if (CONFIG.WIDE_MODE && subsectionMeanings) {
  1125. const wrapper = document.createElement('div');
  1126. wrapper.style.display = 'flex';
  1127. wrapper.style.alignItems = 'flex-start';
  1128. styleSheet.insertRule('.subsection-meanings { max-width: none !important; }', styleSheet.cssRules.length);
  1129.  
  1130. const originalContentWrapper = document.createElement('div');
  1131. originalContentWrapper.style.flex = '1';
  1132. originalContentWrapper.appendChild(subsectionMeanings);
  1133.  
  1134. if (subsectionComposedOfKanji) {
  1135. const newline1 = document.createElement('br');
  1136. originalContentWrapper.appendChild(newline1);
  1137. originalContentWrapper.appendChild(subsectionComposedOfKanji);
  1138. }
  1139. if (subsectionPitchAccent) {
  1140. const newline2 = document.createElement('br');
  1141. originalContentWrapper.appendChild(newline2);
  1142. originalContentWrapper.appendChild(subsectionPitchAccent);
  1143. }
  1144.  
  1145. if (CONFIG.DEFINITIONS_ON_RIGHT_IN_WIDE_MODE) {
  1146. wrapper.appendChild(containerDiv);
  1147. wrapper.appendChild(originalContentWrapper);
  1148. } else {
  1149. wrapper.appendChild(originalContentWrapper);
  1150. wrapper.appendChild(containerDiv);
  1151. }
  1152.  
  1153. if (vboxGap) {
  1154. const existingDynamicDiv = vboxGap.querySelector('#dynamic-content');
  1155. if (existingDynamicDiv) {
  1156. existingDynamicDiv.remove();
  1157. }
  1158.  
  1159. const dynamicDiv = document.createElement('div');
  1160. dynamicDiv.id = 'dynamic-content';
  1161. dynamicDiv.appendChild(wrapper);
  1162.  
  1163. if (window.location.href.includes('vocabulary')) {
  1164. vboxGap.insertBefore(dynamicDiv, vboxGap.children[1]);
  1165. } else {
  1166. vboxGap.insertBefore(dynamicDiv, vboxGap.firstChild);
  1167. }
  1168. }
  1169. } else {
  1170. if (state.embedAboveSubsectionMeanings && subsectionMeanings) {
  1171. subsectionMeanings.parentNode.insertBefore(containerDiv, subsectionMeanings);
  1172. } else if (resultVocabularySection) {
  1173. resultVocabularySection.parentNode.insertBefore(containerDiv, resultVocabularySection);
  1174. } else if (hboxWrapSection) {
  1175. hboxWrapSection.parentNode.insertBefore(containerDiv, hboxWrapSection);
  1176. } else if (subsectionLabels.length >= 4) {
  1177. subsectionLabels[3].parentNode.insertBefore(containerDiv, subsectionLabels[3]);
  1178. }
  1179. }
  1180. }
  1181.  
  1182. function embedImageAndPlayAudio() {
  1183. // Embed the image and play audio, removing existing navigation div if present
  1184. const existingNavigationDiv = document.getElementById('immersion-kit-embed');
  1185. if (existingNavigationDiv) existingNavigationDiv.remove();
  1186.  
  1187. renderImageAndPlayAudio(state.vocab, CONFIG.AUTO_PLAY_SOUND);
  1188. preloadImages();
  1189. }
  1190.  
  1191. function replaceSpecialCharacters(text) {
  1192. // Replace special characters in the text
  1193. return text.replace(/<br>/g, '\n').replace(/&quot;/g, '"').replace(/\n/g, '<br>');
  1194. }
  1195.  
  1196. function preloadImages() {
  1197. // Preload images around the current example index
  1198. const preloadDiv = GM_addElement(document.body, 'div', { style: 'display: none;' });
  1199. const startIndex = Math.max(0, state.currentExampleIndex - CONFIG.NUMBER_OF_PRELOADS);
  1200. const endIndex = Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUMBER_OF_PRELOADS);
  1201.  
  1202. for (let i = startIndex; i <= endIndex; i++) {
  1203. if (!state.preloadedIndices.has(i) && state.examples[i].image_url) {
  1204. GM_addElement(preloadDiv, 'img', { src: state.examples[i].image_url });
  1205. state.preloadedIndices.add(i);
  1206. }
  1207. }
  1208. }
  1209.  
  1210.  
  1211. //MENU FUNCTIONS=====================================================================================================================
  1212. ////FILE OPERATIONS=====================================================================================================================
  1213. function handleImportButtonClick() {
  1214. handleFileInput('application/json', importFavorites);
  1215. }
  1216.  
  1217. function handleImportDButtonClick() {
  1218. handleFileInput('application/json', importData);
  1219. }
  1220.  
  1221. function handleFileInput(acceptType, callback) {
  1222. const fileInput = document.createElement('input');
  1223. fileInput.type = 'file';
  1224. fileInput.accept = acceptType;
  1225. fileInput.addEventListener('change', callback);
  1226. fileInput.click();
  1227. }
  1228.  
  1229. function createBlobAndDownload(data, filename, type) {
  1230. const blob = new Blob([data], { type });
  1231. const url = URL.createObjectURL(blob);
  1232. const a = document.createElement('a');
  1233. a.href = url;
  1234. a.download = filename;
  1235. document.body.appendChild(a);
  1236. a.click();
  1237. document.body.removeChild(a);
  1238. URL.revokeObjectURL(url);
  1239. }
  1240.  
  1241. function addBlacklist() {
  1242. setItem(state.vocab, `0,2`);
  1243. location.reload();
  1244. }
  1245.  
  1246. function remBlacklist() {
  1247. removeItem(state.vocab);
  1248. location.reload();
  1249. }
  1250.  
  1251. function exportFavorites() {
  1252. const favorites = {};
  1253. for (let i = 0; i < localStorage.length; i++) {
  1254. const key = localStorage.key(i);
  1255. if (key.startsWith(scriptPrefix)) {
  1256. const keyPrefixless = key.substring(scriptPrefix.length); // chop off the script prefix
  1257. if (!keyPrefixless.startsWith(configPrefix)) {
  1258. favorites[keyPrefixless] = localStorage.getItem(key);
  1259. // For backwards compatibility keep the exported keys prefixless
  1260. }
  1261. }
  1262. }
  1263. const data = JSON.stringify(favorites, null, 2);
  1264. createBlobAndDownload(data, 'favorites.json', 'application/json');
  1265. }
  1266.  
  1267. function importFavorites(event) {
  1268. const file = event.target.files[0];
  1269. if (!file) return;
  1270.  
  1271. const reader = new FileReader();
  1272. reader.onload = function(e) {
  1273. try {
  1274. const favorites = JSON.parse(e.target.result);
  1275. for (const key in favorites) {
  1276. setItem(key, favorites[key]);
  1277. }
  1278. alert('Favorites imported successfully!');
  1279. location.reload();
  1280. } catch (error) {
  1281. alert('Error importing favorites:', error);
  1282. }
  1283. };
  1284. reader.readAsText(file);
  1285. }
  1286.  
  1287. async function exportData() {
  1288. const dataEntries = {};
  1289.  
  1290. try {
  1291. const db = await IndexedDBManager.open();
  1292. const indexedDBData = await IndexedDBManager.getAll(db);
  1293. indexedDBData.forEach(item => {
  1294. dataEntries[item.keyword] = item.data;
  1295. });
  1296.  
  1297. const data = JSON.stringify(dataEntries, null, 2);
  1298. createBlobAndDownload(data, 'data.json', 'application/json');
  1299. } catch (error) {
  1300. console.error('Error exporting data from IndexedDB:', error);
  1301. }
  1302. }
  1303.  
  1304. async function importData(event) {
  1305. const file = event.target.files[0];
  1306. if (!file) return;
  1307.  
  1308. const reader = new FileReader();
  1309. reader.onload = async function(e) {
  1310. try {
  1311. const dataEntries = JSON.parse(e.target.result);
  1312.  
  1313. const db = await IndexedDBManager.open();
  1314. for (const key in dataEntries) {
  1315. await IndexedDBManager.save(db, key, dataEntries[key]);
  1316. }
  1317.  
  1318. alert('Data imported successfully!');
  1319. location.reload();
  1320. } catch (error) {
  1321. alert('Error importing data:', error);
  1322. }
  1323. };
  1324. reader.readAsText(file);
  1325. }
  1326.  
  1327.  
  1328. ////CONFIRMATION
  1329. function createConfirmationPopup(messageText, onYes, onNo) {
  1330. // Create a confirmation popup with Yes and No buttons
  1331. const popupOverlay = document.createElement('div');
  1332. popupOverlay.style.position = 'fixed';
  1333. popupOverlay.style.top = '0';
  1334. popupOverlay.style.left = '0';
  1335. popupOverlay.style.width = '100%';
  1336. popupOverlay.style.height = '100%';
  1337. popupOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
  1338. popupOverlay.style.zIndex = '1001';
  1339. popupOverlay.style.display = 'flex';
  1340. popupOverlay.style.justifyContent = 'center';
  1341. popupOverlay.style.alignItems = 'center';
  1342.  
  1343. const popupContent = document.createElement('div');
  1344. popupContent.style.backgroundColor = 'var(--background-color)';
  1345. popupContent.style.padding = '20px';
  1346. popupContent.style.borderRadius = '5px';
  1347. popupContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
  1348. popupContent.style.textAlign = 'center';
  1349.  
  1350. const message = document.createElement('p');
  1351. message.textContent = messageText;
  1352.  
  1353. const yesButton = document.createElement('button');
  1354. yesButton.textContent = 'Yes';
  1355. yesButton.style.backgroundColor = '#C82800';
  1356. yesButton.style.marginRight = '10px';
  1357. yesButton.addEventListener('click', () => {
  1358. onYes();
  1359. document.body.removeChild(popupOverlay);
  1360. });
  1361.  
  1362. const noButton = document.createElement('button');
  1363. noButton.textContent = 'No';
  1364. noButton.addEventListener('click', () => {
  1365. onNo();
  1366. document.body.removeChild(popupOverlay);
  1367. });
  1368.  
  1369. popupContent.appendChild(message);
  1370. popupContent.appendChild(yesButton);
  1371. popupContent.appendChild(noButton);
  1372. popupOverlay.appendChild(popupContent);
  1373.  
  1374. document.body.appendChild(popupOverlay);
  1375. }
  1376.  
  1377. ////BUTTONS
  1378. function createActionButtonsContainer() {
  1379. const actionButtonWidth = '100px';
  1380.  
  1381. const closeButton = createButton('Close', '10px', closeOverlayMenu, actionButtonWidth);
  1382. const saveButton = createButton('Save', '10px', saveConfig, actionButtonWidth);
  1383. const defaultButton = createDefaultButton(actionButtonWidth);
  1384. const deleteButton = createDeleteButton(actionButtonWidth);
  1385.  
  1386. const actionButtonsContainer = document.createElement('div');
  1387. actionButtonsContainer.style.textAlign = 'center';
  1388. actionButtonsContainer.style.marginTop = '10px';
  1389. actionButtonsContainer.append(closeButton, saveButton, defaultButton, deleteButton);
  1390.  
  1391. return actionButtonsContainer;
  1392. }
  1393.  
  1394. function createMenuButtons() {
  1395. const blacklistContainer = createBlacklistContainer();
  1396. const favoritesContainer = createFavoritesContainer();
  1397. const dataContainer = createDataContainer();
  1398. const actionButtonsContainer = createActionButtonsContainer();
  1399.  
  1400. const buttonContainer = document.createElement('div');
  1401. buttonContainer.append(blacklistContainer,favoritesContainer,dataContainer,actionButtonsContainer);
  1402.  
  1403. return buttonContainer;
  1404. }
  1405.  
  1406. function createButton(text, margin, onClick, width) {
  1407. // Create a button element with specified properties
  1408. const button = document.createElement('button');
  1409. button.textContent = text;
  1410. button.style.margin = margin;
  1411. button.style.width = width;
  1412. button.style.textAlign = 'center';
  1413. button.style.display = 'inline-block';
  1414. button.style.lineHeight = '30px';
  1415. button.style.padding = '5px 0';
  1416. button.addEventListener('click', onClick);
  1417. return button;
  1418. }
  1419.  
  1420. ////BLACKLIST BUTTONS
  1421. function createBlacklistContainer() {
  1422. const blacklistButtonWidth = '200px';
  1423.  
  1424. const addBlacklistButton = createButton('Add to Blacklist', '10px', addBlacklist, blacklistButtonWidth);
  1425. const remBlacklistButton = createButton('Remove from Blacklist', '10px', remBlacklist, blacklistButtonWidth);
  1426.  
  1427. const blacklistContainer = document.createElement('div');
  1428. blacklistContainer.style.textAlign = 'center';
  1429. blacklistContainer.style.marginTop = '10px';
  1430. blacklistContainer.append(addBlacklistButton, remBlacklistButton);
  1431.  
  1432. return blacklistContainer;
  1433. }
  1434. ////FAVORITE BUTTONS
  1435. function createFavoritesContainer() {
  1436. const favoritesButtonWidth = '200px';
  1437.  
  1438. const exportButton = createButton('Export Favorites', '10px', exportFavorites, favoritesButtonWidth);
  1439. const importButton = createButton('Import Favorites', '10px', handleImportButtonClick, favoritesButtonWidth);
  1440.  
  1441. const favoritesContainer = document.createElement('div');
  1442. favoritesContainer.style.textAlign = 'center';
  1443. favoritesContainer.style.marginTop = '10px';
  1444. favoritesContainer.append(exportButton, importButton);
  1445.  
  1446. return favoritesContainer;
  1447.  
  1448. }
  1449. ////DATA BUTTONS
  1450. function createDataContainer() {
  1451. const dataButtonWidth = '200px';
  1452.  
  1453. const exportButton = createButton('Export Data', '10px', exportData, dataButtonWidth);
  1454. const importButton = createButton('Import Data', '10px', handleImportDButtonClick, dataButtonWidth);
  1455.  
  1456. const dataContainer = document.createElement('div');
  1457. dataContainer.style.textAlign = 'center';
  1458. dataContainer.style.marginTop = '10px';
  1459. dataContainer.append(exportButton, importButton);
  1460.  
  1461. return dataContainer;
  1462. }
  1463.  
  1464. ////CLOSE BUTTON
  1465. function closeOverlayMenu() {
  1466. loadConfig();
  1467. document.body.removeChild(document.getElementById('overlayMenu'));
  1468. }
  1469.  
  1470. ////SAVE BUTTON
  1471. function saveConfig() {
  1472. const overlay = document.getElementById('overlayMenu');
  1473. if (!overlay) return;
  1474.  
  1475. const inputs = overlay.querySelectorAll('input, span');
  1476. const { changes, minimumExampleLengthChanged, newMinimumExampleLength } = gatherChanges(inputs);
  1477.  
  1478. if (minimumExampleLengthChanged) {
  1479. handleMinimumExampleLengthChange(newMinimumExampleLength, changes);
  1480. } else {
  1481. applyChanges(changes);
  1482. finalizeSaveConfig();
  1483. setVocabSize();
  1484. setPageWidth();
  1485. }
  1486. }
  1487.  
  1488. function gatherChanges(inputs) {
  1489. let minimumExampleLengthChanged = false;
  1490. let newMinimumExampleLength;
  1491. const changes = {};
  1492.  
  1493. inputs.forEach(input => {
  1494. const key = input.getAttribute('data-key');
  1495. const type = input.getAttribute('data-type');
  1496. let value;
  1497.  
  1498. if (type === 'boolean') {
  1499. value = input.checked;
  1500. } else if (type === 'number') {
  1501. value = parseFloat(input.textContent);
  1502. } else if (type === 'string') {
  1503. value = input.textContent;
  1504. }
  1505.  
  1506. if (key && type) {
  1507. const typePart = input.getAttribute('data-type-part');
  1508. const originalFormattedType = typePart.slice(1, -1);
  1509.  
  1510. if (key === 'MINIMUM_EXAMPLE_LENGTH' && CONFIG.MINIMUM_EXAMPLE_LENGTH !== value) {
  1511. minimumExampleLengthChanged = true;
  1512. newMinimumExampleLength = value;
  1513. }
  1514.  
  1515. changes[configPrefix + key] = value + originalFormattedType;
  1516. }
  1517. });
  1518.  
  1519. return { changes, minimumExampleLengthChanged, newMinimumExampleLength };
  1520. }
  1521.  
  1522. function handleMinimumExampleLengthChange(newMinimumExampleLength, changes) {
  1523. createConfirmationPopup(
  1524. 'Changing Minimum Example Length will break your current favorites. They will all be deleted. Are you sure?',
  1525. async () => {
  1526. await IndexedDBManager.delete();
  1527. CONFIG.MINIMUM_EXAMPLE_LENGTH = newMinimumExampleLength;
  1528. setItem(`${configPrefix}MINIMUM_EXAMPLE_LENGTH`, newMinimumExampleLength);
  1529. applyChanges(changes);
  1530. clearNonConfigLocalStorage();
  1531. finalizeSaveConfig();
  1532. location.reload();
  1533. },
  1534. () => {
  1535. const overlay = document.getElementById('overlayMenu');
  1536. document.body.removeChild(overlay);
  1537. document.body.appendChild(createOverlayMenu());
  1538. }
  1539. );
  1540. }
  1541.  
  1542. function clearNonConfigLocalStorage() {
  1543. for (let i = 0; i < localStorage.length; i++) {
  1544. const key = localStorage.key(i);
  1545. if (key && key.startsWith(scriptPrefix) && !key.startsWith(scriptPrefix + configPrefix)) {
  1546. localStorage.removeItem(key);
  1547. i--; // Adjust index after removal
  1548. }
  1549. }
  1550. }
  1551.  
  1552. function applyChanges(changes) {
  1553. for (const key in changes) {
  1554. setItem(key, changes[key]);
  1555. }
  1556. }
  1557.  
  1558. function finalizeSaveConfig() {
  1559. loadConfig();
  1560. renderImageAndPlayAudio(state.vocab, CONFIG.AUTO_PLAY_SOUND);
  1561. const overlay = document.getElementById('overlayMenu');
  1562. if (overlay) {
  1563. document.body.removeChild(overlay);
  1564. }
  1565. }
  1566.  
  1567.  
  1568. ////DEFAULT BUTTON
  1569. function createDefaultButton(width) {
  1570. const defaultButton = createButton('Default', '10px', () => {
  1571. createConfirmationPopup(
  1572. 'This will reset all your settings to default. Are you sure?',
  1573. () => {
  1574. Object.keys(localStorage).forEach(key => {
  1575. if (key.startsWith(scriptPrefix + configPrefix)) {
  1576. localStorage.removeItem(key);
  1577. }
  1578. });
  1579. location.reload();
  1580. },
  1581. () => {
  1582. const overlay = document.getElementById('overlayMenu');
  1583. if (overlay) {
  1584. document.body.removeChild(overlay);
  1585. }
  1586. loadConfig();
  1587. document.body.appendChild(createOverlayMenu());
  1588. }
  1589. );
  1590. }, width);
  1591. defaultButton.style.backgroundColor = '#C82800';
  1592. defaultButton.style.color = 'white';
  1593. return defaultButton;
  1594. }
  1595.  
  1596.  
  1597. ////DELETE BUTTON
  1598. function createDeleteButton(width) {
  1599. const deleteButton = createButton('DELETE', '10px', () => {
  1600. createConfirmationPopup(
  1601. 'This will delete all your favorites and cached data. Are you sure?',
  1602. async () => {
  1603. await IndexedDBManager.delete();
  1604. Object.keys(localStorage).forEach(key => {
  1605. if (key.startsWith(scriptPrefix) && !key.startsWith(scriptPrefix + configPrefix)) {
  1606. localStorage.removeItem(key);
  1607. }
  1608. });
  1609. location.reload();
  1610. },
  1611. () => {
  1612. const overlay = document.getElementById('overlayMenu');
  1613. if (overlay) {
  1614. document.body.removeChild(overlay);
  1615. }
  1616. loadConfig();
  1617. document.body.appendChild(createOverlayMenu());
  1618. }
  1619. );
  1620. }, width);
  1621. deleteButton.style.backgroundColor = '#C82800';
  1622. deleteButton.style.color = 'white';
  1623. return deleteButton;
  1624. }
  1625.  
  1626. function createOverlayMenu() {
  1627. // Create and return the overlay menu for configuration settings
  1628. const overlay = document.createElement('div');
  1629. overlay.id = 'overlayMenu';
  1630. overlay.style.position = 'fixed';
  1631. overlay.style.top = '0';
  1632. overlay.style.left = '0';
  1633. overlay.style.width = '100%';
  1634. overlay.style.height = '100%';
  1635. overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
  1636. overlay.style.zIndex = '1000';
  1637. overlay.style.display = 'flex';
  1638. overlay.style.justifyContent = 'center';
  1639. overlay.style.alignItems = 'center';
  1640.  
  1641. const menuContent = document.createElement('div');
  1642. menuContent.style.backgroundColor = 'var(--background-color)';
  1643. menuContent.style.color = 'var(--text-color)';
  1644. menuContent.style.padding = '20px';
  1645. menuContent.style.borderRadius = '5px';
  1646. menuContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
  1647. menuContent.style.width = '80%';
  1648. menuContent.style.maxWidth = '550px';
  1649. menuContent.style.maxHeight = '80%';
  1650. menuContent.style.overflowY = 'auto';
  1651.  
  1652. for (const [key, value] of Object.entries(CONFIG)) {
  1653. const optionContainer = document.createElement('div');
  1654. optionContainer.style.marginBottom = '10px';
  1655. optionContainer.style.display = 'flex';
  1656. optionContainer.style.alignItems = 'center';
  1657.  
  1658. const leftContainer = document.createElement('div');
  1659. leftContainer.style.flex = '1';
  1660. leftContainer.style.display = 'flex';
  1661. leftContainer.style.alignItems = 'center';
  1662.  
  1663. const rightContainer = document.createElement('div');
  1664. rightContainer.style.flex = '1';
  1665. rightContainer.style.display = 'flex';
  1666. rightContainer.style.alignItems = 'center';
  1667. rightContainer.style.justifyContent = 'center';
  1668.  
  1669. const label = document.createElement('label');
  1670. label.textContent = key.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
  1671. label.style.marginRight = '10px';
  1672.  
  1673. leftContainer.appendChild(label);
  1674.  
  1675. if (typeof value === 'boolean') {
  1676. const checkboxContainer = document.createElement('div');
  1677. checkboxContainer.style.display = 'flex';
  1678. checkboxContainer.style.alignItems = 'center';
  1679. checkboxContainer.style.justifyContent = 'center';
  1680.  
  1681. const checkbox = document.createElement('input');
  1682. checkbox.type = 'checkbox';
  1683. checkbox.checked = value;
  1684. checkbox.setAttribute('data-key', key);
  1685. checkbox.setAttribute('data-type', 'boolean');
  1686. checkbox.setAttribute('data-type-part', '');
  1687. checkboxContainer.appendChild(checkbox);
  1688.  
  1689. rightContainer.appendChild(checkboxContainer);
  1690. } else if (typeof value === 'number') {
  1691. const numberContainer = document.createElement('div');
  1692. numberContainer.style.display = 'flex';
  1693. numberContainer.style.alignItems = 'center';
  1694. numberContainer.style.justifyContent = 'center';
  1695.  
  1696. const decrementButton = document.createElement('button');
  1697. decrementButton.textContent = '-';
  1698. decrementButton.style.marginRight = '5px';
  1699.  
  1700. const input = document.createElement('span');
  1701. input.textContent = value;
  1702. input.style.margin = '0 10px';
  1703. input.style.minWidth = '3ch';
  1704. input.style.textAlign = 'center';
  1705. input.setAttribute('data-key', key);
  1706. input.setAttribute('data-type', 'number');
  1707. input.setAttribute('data-type-part', '');
  1708.  
  1709. const incrementButton = document.createElement('button');
  1710. incrementButton.textContent = '+';
  1711. incrementButton.style.marginLeft = '5px';
  1712.  
  1713. const updateButtonStates = () => {
  1714. let currentValue = parseFloat(input.textContent);
  1715. if (currentValue <= 0) {
  1716. decrementButton.disabled = true;
  1717. decrementButton.style.color = 'grey';
  1718. } else {
  1719. decrementButton.disabled = false;
  1720. decrementButton.style.color = '';
  1721. }
  1722. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  1723. incrementButton.disabled = true;
  1724. incrementButton.style.color = 'grey';
  1725. } else {
  1726. incrementButton.disabled = false;
  1727. incrementButton.style.color = '';
  1728. }
  1729. };
  1730.  
  1731. decrementButton.addEventListener('click', () => {
  1732. let currentValue = parseFloat(input.textContent);
  1733. if (currentValue > 0) {
  1734. if (currentValue > 200) {
  1735. input.textContent = currentValue - 25;
  1736. } else if (currentValue > 20) {
  1737. input.textContent = currentValue - 5;
  1738. } else {
  1739. input.textContent = currentValue - 1;
  1740. }
  1741. updateButtonStates();
  1742. }
  1743. });
  1744.  
  1745. incrementButton.addEventListener('click', () => {
  1746. let currentValue = parseFloat(input.textContent);
  1747. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  1748. return;
  1749. }
  1750. if (currentValue >= 200) {
  1751. input.textContent = currentValue + 25;
  1752. } else if (currentValue >= 20) {
  1753. input.textContent = currentValue + 5;
  1754. } else {
  1755. input.textContent = currentValue + 1;
  1756. }
  1757. updateButtonStates();
  1758. });
  1759.  
  1760. numberContainer.appendChild(decrementButton);
  1761. numberContainer.appendChild(input);
  1762. numberContainer.appendChild(incrementButton);
  1763.  
  1764. rightContainer.appendChild(numberContainer);
  1765.  
  1766. // Initialize button states
  1767. updateButtonStates();
  1768. } else if (typeof value === 'string') {
  1769. const typeParts = value.split(/(\d+)/).filter(Boolean);
  1770. const numberParts = typeParts.filter(part => !isNaN(part)).map(Number);
  1771.  
  1772. const numberContainer = document.createElement('div');
  1773. numberContainer.style.display = 'flex';
  1774. numberContainer.style.alignItems = 'center';
  1775. numberContainer.style.justifyContent = 'center';
  1776.  
  1777. const typeSpan = document.createElement('span');
  1778. const formattedType = '(' + typeParts.filter(part => isNaN(part)).join('').replace(/_/g, ' ').toLowerCase() + ')';
  1779. typeSpan.textContent = formattedType;
  1780. typeSpan.style.marginRight = '10px';
  1781.  
  1782. leftContainer.appendChild(typeSpan);
  1783.  
  1784. typeParts.forEach(part => {
  1785. if (!isNaN(part)) {
  1786. const decrementButton = document.createElement('button');
  1787. decrementButton.textContent = '-';
  1788. decrementButton.style.marginRight = '5px';
  1789.  
  1790. const input = document.createElement('span');
  1791. input.textContent = part;
  1792. input.style.margin = '0 10px';
  1793. input.style.minWidth = '3ch';
  1794. input.style.textAlign = 'center';
  1795. input.setAttribute('data-key', key);
  1796. input.setAttribute('data-type', 'string');
  1797. input.setAttribute('data-type-part', formattedType);
  1798.  
  1799. const incrementButton = document.createElement('button');
  1800. incrementButton.textContent = '+';
  1801. incrementButton.style.marginLeft = '5px';
  1802.  
  1803. const updateButtonStates = () => {
  1804. let currentValue = parseFloat(input.textContent);
  1805. if (currentValue <= 0) {
  1806. decrementButton.disabled = true;
  1807. decrementButton.style.color = 'grey';
  1808. } else {
  1809. decrementButton.disabled = false;
  1810. decrementButton.style.color = '';
  1811. }
  1812. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  1813. incrementButton.disabled = true;
  1814. incrementButton.style.color = 'grey';
  1815. } else {
  1816. incrementButton.disabled = false;
  1817. incrementButton.style.color = '';
  1818. }
  1819. };
  1820.  
  1821. decrementButton.addEventListener('click', () => {
  1822. let currentValue = parseFloat(input.textContent);
  1823. if (currentValue > 0) {
  1824. if (currentValue > 200) {
  1825. input.textContent = currentValue - 25;
  1826. } else if (currentValue > 20) {
  1827. input.textContent = currentValue - 5;
  1828. } else {
  1829. input.textContent = currentValue - 1;
  1830. }
  1831. updateButtonStates();
  1832. }
  1833. });
  1834.  
  1835. incrementButton.addEventListener('click', () => {
  1836. let currentValue = parseFloat(input.textContent);
  1837. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  1838. return;
  1839. }
  1840. if (currentValue >= 200) {
  1841. input.textContent = currentValue + 25;
  1842. } else if (currentValue >= 20) {
  1843. input.textContent = currentValue + 5;
  1844. } else {
  1845. input.textContent = currentValue + 1;
  1846. }
  1847. updateButtonStates();
  1848. });
  1849.  
  1850. numberContainer.appendChild(decrementButton);
  1851. numberContainer.appendChild(input);
  1852. numberContainer.appendChild(incrementButton);
  1853.  
  1854. // Initialize button states
  1855. updateButtonStates();
  1856. }
  1857. });
  1858.  
  1859. rightContainer.appendChild(numberContainer);
  1860. }
  1861.  
  1862. optionContainer.appendChild(leftContainer);
  1863. optionContainer.appendChild(rightContainer);
  1864. menuContent.appendChild(optionContainer);
  1865. }
  1866.  
  1867. const menuButtons = createMenuButtons();
  1868. menuContent.appendChild(menuButtons);
  1869.  
  1870. overlay.appendChild(menuContent);
  1871.  
  1872. return overlay;
  1873. }
  1874.  
  1875. function loadConfig() {
  1876. for (const key in localStorage) {
  1877. if (!key.startsWith(scriptPrefix + configPrefix) || !localStorage.hasOwnProperty(key)) {continue};
  1878.  
  1879. const configKey = key.substring((scriptPrefix + configPrefix).length); // chop off script prefix and config prefix
  1880. if (!CONFIG.hasOwnProperty(configKey)) {continue};
  1881.  
  1882. const savedValue = localStorage.getItem(key);
  1883. if (savedValue === null) {continue};
  1884.  
  1885. const valueType = typeof CONFIG[configKey];
  1886. if (valueType === 'boolean') {
  1887. CONFIG[configKey] = savedValue === 'true';
  1888. if (configKey === "DEFAULT_TO_EXACT_SEARCH") { state.exactSearch = CONFIG.DEFAULT_TO_EXACT_SEARCH }
  1889. // I wonder if this is the best way to do this...
  1890. // Probably not because we could just have a single variable to store both, but it would have to be in config and
  1891. // it would be a bit weird to have the program modifying config when the actual config settings aren't changing
  1892. } else if (valueType === 'number') {
  1893. CONFIG[configKey] = parseFloat(savedValue);
  1894. } else if (valueType === 'string') {
  1895. CONFIG[configKey] = savedValue;
  1896. }
  1897. }
  1898. }
  1899.  
  1900.  
  1901. //MAIN FUNCTIONS=====================================================================================================================
  1902. function onPageLoad() {
  1903. // Initialize state and determine vocabulary based on URL
  1904. state.embedAboveSubsectionMeanings = false;
  1905.  
  1906. const url = window.location.href;
  1907. const machineTranslationFrame = document.getElementById('machine-translation-frame');
  1908.  
  1909. // Proceed only if the machine translation frame is not present
  1910. if (!machineTranslationFrame) {
  1911.  
  1912. //display embed for first time with loading text
  1913. embedImageAndPlayAudio();
  1914. setPageWidth();
  1915.  
  1916. if (url.includes('/vocabulary/')) {
  1917. state.vocab = parseVocabFromVocabulary();
  1918. } else if (url.includes('/search?q=')) {
  1919. state.vocab = parseVocabFromSearch();
  1920. } else if (url.includes('c=')) {
  1921. state.vocab = parseVocabFromAnswer();
  1922. } else if (url.includes('/kanji/')) {
  1923. state.vocab = parseVocabFromKanji();
  1924. } else {
  1925. state.vocab = parseVocabFromReview();
  1926. }
  1927. } else {
  1928. console.log('Machine translation frame detected, skipping vocabulary parsing.');
  1929. }
  1930.  
  1931. // Retrieve stored data for the current vocabulary
  1932. const { index, exactState } = getStoredData(state.vocab);
  1933. state.currentExampleIndex = index;
  1934. state.exactSearch = exactState;
  1935.  
  1936. // Fetch data and embed image/audio if necessary
  1937. if (state.vocab && !state.apiDataFetched) {
  1938. getImmersionKitData(state.vocab, state.exactSearch)
  1939. .then(() => {
  1940. preloadImages();
  1941. embedImageAndPlayAudio();
  1942. })
  1943. .catch(console.error);
  1944. } else if (state.apiDataFetched) {
  1945. embedImageAndPlayAudio();
  1946. //preloadImages();
  1947. setVocabSize();
  1948. setPageWidth();
  1949. }
  1950. }
  1951.  
  1952. function setPageWidth() {
  1953. // Set the maximum width of the page
  1954. document.body.style.maxWidth = CONFIG.PAGE_WIDTH;
  1955. }
  1956.  
  1957. // Observe URL changes and reload the page content accordingly
  1958. const observer = new MutationObserver(() => {
  1959. if (window.location.href !== observer.lastUrl) {
  1960. observer.lastUrl = window.location.href;
  1961. onPageLoad();
  1962. }
  1963. });
  1964.  
  1965. // Function to apply styles
  1966. function setVocabSize() {
  1967. // Create a new style element
  1968. const style = document.createElement('style');
  1969. style.type = 'text/css';
  1970. style.innerHTML = `
  1971. .answer-box > .plain {
  1972. font-size: ${CONFIG.VOCAB_SIZE} !important; /* Use the configurable font size */
  1973. padding-bottom: 0.1rem !important; /* Retain padding */
  1974. }
  1975. `;
  1976.  
  1977. // Append the new style to the document head
  1978. document.head.appendChild(style);
  1979. }
  1980. observer.lastUrl = window.location.href;
  1981. observer.observe(document, { subtree: true, childList: true });
  1982.  
  1983. // Add event listeners for page load and URL changes
  1984. window.addEventListener('load', onPageLoad);
  1985. window.addEventListener('popstate', onPageLoad);
  1986. window.addEventListener('hashchange', onPageLoad);
  1987.  
  1988. // Initial configuration and preloading
  1989. loadConfig();
  1990. setPageWidth();
  1991. setVocabSize();
  1992. //preloadImages();
  1993.  
  1994. })();