JPDB Nadeshiko Examples

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

目前為 2025-03-14 提交的版本,檢視 最新版本

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