JPDB Nadeshiko Examples

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

目前为 2025-04-15 提交的版本,查看 最新版本

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