JPDB Immersion Kit Examples

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

目前为 2024-10-09 提交的版本。查看 最新版本

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