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-09-20 提交的版本。查看 最新版本

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