JPDB Nadeshiko Examples

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

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