Greasy Fork 还支持 简体中文。

JPDB Nadeshiko Examples

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

  1. // ==UserScript==
  2. // @name JPDB Nadeshiko Examples
  3. // @version 1.23
  4. // @description Embeds anime images & audio examples into JPDB review and vocabulary pages using Nadeshiko's API. Compatible only with TamperMonkey.
  5. // @author awoo& Sacus
  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. // @match https://jpdb.io/deck*
  12. // @connect api.brigadasos.xyz
  13. // @connect sargus.fr
  14. // @grant GM_addElement
  15. // @grant GM_xmlhttpRequest
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // @grant GM_registerMenuCommand
  19. // @license MIT
  20. // ==/UserScript==
  21. /*jshint esversion: 11 */
  22. /* global GM_addElement, GM_xmlhttpRequest, GM_setValue, GM_getValue, GM_registerMenuCommand */
  23. (function () {
  24. 'use strict';
  25. let nadeshikoApiKey = GM_getValue("nadeshiko-api-key", "");
  26. let jpdbApiKey = GM_getValue("jpdb-api-key", "");
  27. // Register menu commands
  28. GM_registerMenuCommand("Set Nadeshiko API Key", async () => {
  29. nadeshikoApiKey = fetchNadeshikoApiKey();
  30. });
  31. GM_registerMenuCommand("Set JPDB API Key", async () => {
  32. jpdbApiKey = fetchJPDBApiKey();
  33. });
  34.  
  35. function fetchNadeshikoApiKey() {
  36. 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");
  37. GM_setValue("nadeshiko-api-key", apiKey);
  38.  
  39. if (apiKey) {
  40. alert("API Key saved successfully!");
  41. }
  42.  
  43. return apiKey;
  44. }
  45.  
  46. function fetchJPDBApiKey() {
  47. let apiKey = prompt("A JPDB API key is required for this extension to work.\n\nYou can get it in the settings page of your JPDB account.");
  48. GM_setValue("jpdb-api-key", apiKey);
  49.  
  50. if (apiKey) {
  51. // send ping at https://jpdb.io/api/v1/ping to check if the key is valid
  52. GM_xmlhttpRequest({
  53. method: "POST",
  54. url: "https://jpdb.io/api/v1/ping",
  55. headers: {
  56. "Authorization": `Bearer ${apiKey}`,
  57. },
  58. onload: function (response) {
  59. if (response.status === 200) {
  60. alert("API Key saved successfully!");
  61. } else {
  62. alert("Invalid API Key. Please check your key and try again.");
  63. }
  64. },
  65. onerror: function (error) {
  66. alert("An error occurred while checking the API Key. Please try again.");
  67. }
  68. });
  69. }
  70. return apiKey;
  71. }
  72.  
  73. // to use custom hotkeys just add them into this array following the same format. Any single keys except space
  74. // should work. If you want to use special keys, check the linked page for how to represent them in the array
  75. // (link leads to the arrow keys part so you can compare with the array and be sure which part to write):
  76. // https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#navigation_keys
  77. const hotkeyOptions = ['None', 'ArrowLeft ArrowRight', ', .', '[ ]', 'Q W'];
  78.  
  79. const RANDOM_SENTENCE_ENUM = {
  80. DISABLE: 0,
  81. ON_FIRST: 1,
  82. EVERY_TIME: 2
  83. };
  84. const CONFIG = {
  85. IMAGE_WIDTH: '400px',
  86. WIDE_MODE: true,
  87. DEFINITIONS_ON_RIGHT_IN_WIDE_MODE: false,
  88. ARROW_WIDTH: '75px',
  89. ARROW_HEIGHT: '45px',
  90. PAGE_WIDTH: '75rem',
  91. SOUND_VOLUME: 80,
  92. ENABLE_EXAMPLE_TRANSLATION: true,
  93. SENTENCE_FONT_SIZE: '120%',
  94. TRANSLATION_FONT_SIZE: '85%',
  95. COLORED_SENTENCE_TEXT: true,
  96. AUTO_PLAY_SOUND: true,
  97. NUMBER_OF_PRELOADS: 1,
  98. VOCAB_SIZE: '250%',
  99. MINIMUM_EXAMPLE_LENGTH: 0,
  100. MAXIMUM_EXAMPLE_LENGTH: 100,
  101. HOTKEYS: ['None'],
  102. DEFAULT_TO_EXACT_SEARCH: true,
  103. // On changing this config option, the icons change but the sentences don't, so you
  104. // have to click once to match up the icons and again to actually change the sentences
  105. RANDOM_SENTENCE: RANDOM_SENTENCE_ENUM,
  106. WEIGHTED_SENTENCES: false,
  107. DEBUG: false, // Set to true to not use IndexedDB and always fetch from API
  108. };
  109.  
  110. const state = {
  111. currentExampleIndex: 0,
  112. examples: [],
  113. apiDataFetched: false,
  114. vocab: '',
  115. embedAboveSubsectionMeanings: false,
  116. preloadedIndices: new Set(),
  117. currentAudio: null,
  118. exactSearch: true,
  119. error: false,
  120. currentlyPlayingAudio: false,
  121. reading: '',
  122. };
  123.  
  124. // Prefixing
  125. const scriptPrefix = 'JPDBNadeshikoExamples-';
  126. const configPrefix = 'CONFIG.'; // additional prefix for config variables to go after the scriptPrefix
  127. // do not change either of the above without adding code to handle the change
  128.  
  129. const setItem = (key, value) => {
  130. localStorage.setItem(scriptPrefix + key, value);
  131. };
  132. const getItem = (key) => {
  133. const prefixedValue = localStorage.getItem(scriptPrefix + key);
  134. if (prefixedValue !== null) {
  135. return prefixedValue;
  136. }
  137. const nonPrefixedValue = localStorage.getItem(key);
  138. // to move away from non-prefixed values as fast as possible
  139. if (nonPrefixedValue !== null) {
  140. setItem(key, nonPrefixedValue);
  141. }
  142. return nonPrefixedValue;
  143. };
  144. const removeItem = (key) => {
  145. localStorage.removeItem(scriptPrefix + key);
  146. localStorage.removeItem(key);
  147. };
  148.  
  149. // Helper for transitioning to fully script-prefixed config state
  150. // Deletes all localStorage variables starting with configPrefix and re-adds them with scriptPrefix and configPrefix
  151. // Danger of other scripts also having localStorage variables starting with configPrefix, so we add a flag showing that
  152. // we have run this function and make sure it is not set when running it
  153.  
  154. // Check for Prefixed flag
  155. if (localStorage.getItem(`JPDBNadeshiko*Examples-CONFIG_VARIABLES_PREFIXED`) !== 'true') {
  156. const keysToModify = [];
  157.  
  158. // Collect keys that need to be modified
  159. for (let i = 0; i < localStorage.length; i++) {
  160. const key = localStorage.key(i);
  161. if (key.startsWith(configPrefix)) {
  162. keysToModify.push(key);
  163. }
  164. }
  165.  
  166. // Modify the collected keys
  167. keysToModify.forEach((key) => {
  168. const value = localStorage.getItem(key);
  169. localStorage.removeItem(key);
  170. const newKey = scriptPrefix + key;
  171. localStorage.setItem(newKey, value);
  172. });
  173. // Set flag so this only runs once
  174. // Flag has * in name to place at top in alphabetical sorting,
  175. // and most importantly, to ensure the flag is never removed or modified
  176. // by the other script functions that check for the script prefix
  177. localStorage.setItem(`JPDBNadeshiko*Examples-CONFIG_VARIABLES_PREFIXED`, 'true');
  178. }
  179.  
  180. // IndexedDB Manager
  181. const IndexedDBManager = {
  182. MAX_ENTRIES: 100000000,
  183. EXPIRATION_TIME: 30 * 24 * 60 * 60 * 1000, // 30 days in milliseconds
  184.  
  185. open() {
  186. return new Promise((resolve, reject) => {
  187. const request = indexedDB.open('NadeshikoDB', 1);
  188. request.onupgradeneeded = function (event) {
  189. const db = event.target.result;
  190. if (!db.objectStoreNames.contains('dataStore')) {
  191. db.createObjectStore('dataStore', {keyPath: 'keyword'});
  192. }
  193. };
  194. request.onsuccess = function (event) {
  195. resolve(event.target.result);
  196. };
  197. request.onerror = function (event) {
  198. reject('IndexedDB error: ' + event.target.errorCode);
  199. };
  200. });
  201. },
  202.  
  203. get(db, keyword) {
  204. return new Promise((resolve, reject) => {
  205. const transaction = db.transaction(['dataStore'], 'readonly');
  206. const store = transaction.objectStore('dataStore');
  207. const request = store.get(keyword);
  208. request.onsuccess = async function (event) {
  209. const result = event.target.result;
  210. if (result) {
  211. const isExpired = Date.now() - result.timestamp >= this.EXPIRATION_TIME;
  212. const validationError = validateApiResponse(result.data);
  213.  
  214. if (isExpired) {
  215. console.log(`Deleting entry for keyword "${keyword}" because it is expired.`);
  216. await this.deleteEntry(db, keyword);
  217. resolve(null);
  218. } else if (validationError) {
  219. console.log(`Deleting entry for keyword "${keyword}" due to validation error: ${validationError}`);
  220. await this.deleteEntry(db, keyword);
  221. resolve(null);
  222. } else {
  223. resolve(result.data);
  224. }
  225. } else {
  226. resolve(null);
  227. }
  228. }.bind(this);
  229. request.onerror = function (event) {
  230. reject('IndexedDB get error: ' + event.target.errorCode);
  231. };
  232. });
  233. },
  234.  
  235. deleteEntry(db, keyword) {
  236. return new Promise((resolve, reject) => {
  237. const transaction = db.transaction(['dataStore'], 'readwrite');
  238. const store = transaction.objectStore('dataStore');
  239. const request = store.delete(keyword);
  240. request.onsuccess = () => resolve();
  241. request.onerror = (e) => reject('IndexedDB delete error: ' + e.target.errorCode);
  242. });
  243. },
  244.  
  245.  
  246. getAll(db) {
  247. return new Promise((resolve, reject) => {
  248. const transaction = db.transaction(['dataStore'], 'readonly');
  249. const store = transaction.objectStore('dataStore');
  250. const entries = [];
  251. store.openCursor().onsuccess = function (event) {
  252. const cursor = event.target.result;
  253. if (cursor) {
  254. entries.push(cursor.value);
  255. cursor.continue();
  256. } else {
  257. resolve(entries);
  258. }
  259. };
  260. store.openCursor().onerror = function (event) {
  261. reject('Failed to retrieve entries via cursor: ' + event.target.errorCode);
  262. };
  263. });
  264. },
  265.  
  266. save(db, keyword, data) {
  267. return new Promise(async (resolve, reject) => {
  268. try {
  269. const validationError = validateApiResponse(data);
  270. if (validationError) {
  271. console.log(`Invalid data detected: ${validationError}. Not saving to IndexedDB.`);
  272. resolve();
  273. return;
  274. }
  275. // check for DEBUG mode
  276. if (CONFIG.DEBUG) {
  277. console.log('DEBUG mode is enabled. Not using IndexedDB, always fetching from API.');
  278. resolve();
  279. return;
  280. }
  281. // Transform the JSON object to slim it down
  282. let slimData = {};
  283. if (data) {
  284. slimData = data;
  285. } else {
  286. console.error('Data does not contain expected structure. Cannot slim down.');
  287. resolve();
  288. return;
  289. }
  290.  
  291. const entries = await this.getAll(db);
  292. const transaction = db.transaction(['dataStore'], 'readwrite');
  293. const store = transaction.objectStore('dataStore');
  294.  
  295. if (entries.length >= this.MAX_ENTRIES) {
  296. // Sort entries by timestamp and delete oldest ones
  297. entries.sort((a, b) => a.timestamp - b.timestamp);
  298. const entriesToDelete = entries.slice(0, entries.length - this.MAX_ENTRIES + 1);
  299.  
  300. // Delete old entries
  301. entriesToDelete.forEach(entry => {
  302. store.delete(entry.keyword).onerror = function () {
  303. console.error('Failed to delete entry:', entry.keyword);
  304. };
  305. });
  306. }
  307.  
  308. // Add the new slimmed entry
  309. const addRequest = store.put({keyword, data: slimData, timestamp: Date.now()});
  310. addRequest.onsuccess = () => resolve();
  311. addRequest.onerror = (e) => reject('IndexedDB save error: ' + e.target.errorCode);
  312.  
  313. transaction.oncomplete = function () {
  314. console.log('IndexedDB updated successfully.');
  315. };
  316.  
  317. transaction.onerror = function (event) {
  318. reject('IndexedDB update failed: ' + event.target.errorCode);
  319. };
  320.  
  321. } catch (error) {
  322. reject(`Error in saveToIndexedDB: ${error}`);
  323. }
  324. });
  325. },
  326.  
  327. delete() {
  328. return new Promise((resolve, reject) => {
  329. const request = indexedDB.deleteDatabase('NadeshikoDB');
  330. request.onsuccess = function () {
  331. console.log('IndexedDB deleted successfully');
  332. resolve();
  333. };
  334. request.onerror = function (event) {
  335. console.error('Error deleting IndexedDB:', event.target.errorCode);
  336. reject('Error deleting IndexedDB: ' + event.target.errorCode);
  337. };
  338. request.onblocked = function () {
  339. console.warn('Delete operation blocked. Please close all other tabs with this site open and try again.');
  340. reject('Delete operation blocked');
  341. };
  342. });
  343. }
  344. };
  345.  
  346.  
  347. // API FUNCTIONS=====================================================================================================================
  348. function getNadeshikoData(vocab, exactSearch) {
  349.  
  350.  
  351. return new Promise(async (resolve, reject) => {
  352. const searchVocab = exactSearch ? `"${vocab}"` : vocab;
  353. const url = `https://api.brigadasos.xyz/api/v1/search/media/sentence`;
  354. const maxRetries = 5;
  355. let attempt = 0;
  356.  
  357. const storedValue = getItem(state.vocab);
  358. const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
  359.  
  360. // Return early if not blacklisted
  361. if (isBlacklisted) {
  362. resolve();
  363. return;
  364. }
  365.  
  366. async function fetchData() {
  367. try {
  368. const db = await IndexedDBManager.open();
  369. const cachedData = await IndexedDBManager.get(db, searchVocab);
  370. if (cachedData && Array.isArray(cachedData) && cachedData.length > 0) {
  371. console.log('Data retrieved from IndexedDB');
  372. state.examples = cachedData;
  373. state.apiDataFetched = true;
  374. resolve();
  375. } else {
  376. console.log(`Calling API for: ${searchVocab}`);
  377. if (!nadeshikoApiKey) {
  378. // Ask for API Key on search if not set to prevent 401 errors
  379. nadeshikoApiKey = fetchNadeshikoApiKey();
  380. if (!nadeshikoApiKey) {
  381. return;
  382. }
  383. }
  384.  
  385. GM_xmlhttpRequest({
  386. method: "POST",
  387. url: url,
  388. data: JSON.stringify({
  389. query: searchVocab,
  390. "limit": 500,
  391. "min_length": CONFIG.MINIMUM_EXAMPLE_LENGTH,
  392. "max_length": CONFIG.MAXIMUM_EXAMPLE_LENGTH
  393. }),
  394. headers:
  395. {
  396. "X-API-Key": nadeshikoApiKey,
  397. "Content-Type": "application/json"
  398. },
  399. onload: async function (response) {
  400. console.log(state)
  401. async function validateAndUpdateExamples(jsonState) {
  402. try {
  403. const sargusData = {
  404. word: state?.vocab ?? '',
  405. reading: state?.reading ?? '', // optional reading
  406. sentences: (jsonState ?? []) // guarantee an array
  407. .map(e => e?.segment_info?.content_jp) // optional-chain every step
  408. .filter(Boolean) // keep only truthy strings
  409. };
  410.  
  411. const names = await checkIfNames(sargusData); // may throw/reject
  412. if (!Array.isArray(names)) {
  413. throw new TypeError('checkIfNames did not return an array.');
  414. }
  415. if (names.length !== jsonState.length) {
  416. throw new RangeError(
  417. `Mismatch: received ${names.length} flags for ${jsonState.length} examples.`
  418. );
  419. }
  420.  
  421. jsonState = jsonState.filter((_, i) => !names[i]);
  422. return jsonState; // return filtered examples
  423. } catch (err) {
  424. console.error('Failed to filter examples:', err);
  425. // Decide how to handle the error: show a message, re-throw, etc.
  426. }
  427. }
  428.  
  429. if (response.status === 200) {
  430. let jsonData = parseJSON(response.response).sentences;
  431. console.log("API JSON Received");
  432. const validationError = validateApiResponse(jsonData);
  433. if (!validationError) {
  434. state.apiDataFetched = true;
  435. // check if the sentence is in the vocab
  436. jsonData = await validateAndUpdateExamples(jsonData)
  437. const sentenceResults = await Promise.all(
  438. jsonData.map(async sentence => {
  439. return await preprocessSentence(sentence);
  440. }))
  441. jsonData = sentenceResults.filter(s => s);
  442. state.examples = jsonData;
  443. await IndexedDBManager.save(db, searchVocab, jsonData);
  444. resolve();
  445. } else {
  446. attempt++;
  447. if (attempt < maxRetries) {
  448. console.log(`Validation error: ${validationError}. Retrying... (${attempt}/${maxRetries})`);
  449. setTimeout(fetchData, 5000); // Add a 5-second delay before retrying
  450. } else {
  451. reject(`Invalid API response after ${maxRetries} attempts: ${validationError}`);
  452. state.error = true;
  453. embedImageAndPlayAudio(); //update displayed text
  454. }
  455. }
  456. } else {
  457. reject(`API call failed with status: ${response.status}`);
  458. }
  459. },
  460. onerror: function (error) {
  461. reject(`An error occurred: ${error}`);
  462. }
  463. });
  464. }
  465. } catch (error) {
  466. reject(`Error: ${error}`);
  467. }
  468. }
  469.  
  470. await fetchData();
  471. });
  472. }
  473.  
  474. function parseJSON(responseText) {
  475. try {
  476. return JSON.parse(responseText);
  477. } catch (e) {
  478. console.error('Error parsing JSON:', e);
  479. return null;
  480. }
  481. }
  482.  
  483. function validateApiResponse(jsonData) {
  484. state.error = false;
  485. if (!jsonData) {
  486. return 'Not a valid JSON';
  487. }
  488. const categoryCount = jsonData.length;
  489. if (!categoryCount) {
  490. return 'Missing category count';
  491. }
  492.  
  493. // Check if all category counts are zero
  494. const allZero = categoryCount === 0;
  495. if (allZero) {
  496. return 'Blank API';
  497. }
  498.  
  499. return null; // No error
  500. }
  501.  
  502. async function checkIfNames(sargusData) {
  503. return await new Promise((resolve, reject) => {
  504. GM_xmlhttpRequest({
  505. method: "POST",
  506. url: "http://sargus.fr:8000/api/check_names",
  507. data: JSON.stringify(sargusData),
  508. headers: {
  509. "Content-Type": "application/json"
  510. },
  511. onload: function (response) {
  512. if (response.status === 200) {
  513. try {
  514. const names = JSON.parse(response.responseText);
  515. if (Array.isArray(names)) {
  516. resolve(names);
  517. } else {
  518. console.error("Invalid response format, expected an array:", names);
  519. reject(null);
  520. }
  521. } catch (e) {
  522. console.error("Error parsing response as JSON:", e);
  523. reject(null);
  524. }
  525. } else {
  526. console.error("Error checking if names :", response.responseText);
  527. reject(null);
  528. }
  529. }
  530. });
  531. });
  532. }
  533.  
  534. async function preprocessSentence(sentence) {
  535. const content = sentence.segment_info.content_jp;
  536. // Set weights for each sentence by calling jpdb api (if needed)
  537. if (CONFIG.WEIGHTED_SENTENCES && jpdbApiKey) {
  538. const jipidata = {
  539. text: content,
  540. token_fields: [],
  541. position_length_encoding: "utf16",
  542. vocabulary_fields: ["card_state", "spelling", "reading"]
  543. };
  544. let vocabInSentence = false;
  545. await processJPDBData(sentence, jipidata);
  546.  
  547. async function processJPDBData(sentence, jipidata) {
  548. return await new Promise((resolve) => {
  549. GM_xmlhttpRequest({
  550. method: "POST",
  551. url: "https://jpdb.io/api/v1/parse",
  552. headers: {Authorization: `Bearer ${jpdbApiKey}`},
  553. data: JSON.stringify(jipidata),
  554. onload: function (response) {
  555. let weight = 1;
  556. if (response.status === 200) {
  557. try {
  558. const vocab = JSON.parse(response.responseText).vocabulary || [];
  559. if (vocab.length > 0) {
  560. const VALID_CARD_STATES = ["known", "never-forget", "learning"];
  561. let matchCount = 0;
  562. for (const item of vocab) {
  563. if (!state.reading){
  564. vocabInSentence = true;
  565. }
  566. else if (item && item[1] && item[2] && item[1].includes(state.vocab) && item[2].includes(state.reading)) {
  567. vocabInSentence = true;
  568. }
  569. if (item && item[0] && VALID_CARD_STATES.includes(item[0][0])) {
  570. matchCount++;
  571. }
  572. }
  573. weight = (matchCount * 100 / (vocab.length));
  574. }
  575. } catch {
  576. console.error("Error parsing parse response");
  577. }
  578. }
  579. sentence.weight = weight;
  580. resolve();
  581. },
  582. onerror: function () {
  583. sentence.weight = 1;
  584. resolve();
  585. }
  586. });
  587. });
  588. }
  589.  
  590. // if vocabInSentence is false, remove sentence from the examples
  591. if (!vocabInSentence) {
  592. console.log(`Skipping sentence "${content}" because it does not contain the vocab "${state.vocab}" or reading "${state.reading}".`);
  593. return null;
  594. }
  595. }
  596.  
  597. return sentence;
  598. }
  599.  
  600. //FAVORITE DATA FUNCTIONS=====================================================================================================================
  601. function getStoredData(key) {
  602. // Retrieve the stored value from localStorage using the provided key
  603. const storedValue = getItem(key);
  604.  
  605. // If a stored value exists, split it into index and exactState
  606. if (storedValue) {
  607. const [index, exactState] = storedValue.split(',');
  608. return {
  609. index: parseInt(index, 10), // Convert index to an integer
  610. exactState: exactState === '1' // Convert exactState to a boolean
  611. };
  612. }
  613.  
  614. // Return default values if no stored value exists
  615. return {index: 0, exactState: state.exactSearch};
  616. }
  617.  
  618. function storeData(key, sentence, exactState) {
  619. // Create a string value from index and exactState to store in localStorage
  620. const value = `${sentence},${exactState ? 1 : 0}`;
  621.  
  622. // Store the value in localStorage using the provided key
  623. setItem(key, value);
  624. }
  625.  
  626.  
  627. // PARSE VOCAB FUNCTIONS =====================================================================================================================
  628. function parseVocabFromAnswer() {
  629. // Select all links containing "/kanji/" or "/vocabulary/" in the href attribute
  630. const elements = document.querySelectorAll('a[href*="/kanji/"], a[href*="/vocabulary/"]');
  631. console.log("Parsing Answer Page");
  632.  
  633. // Iterate through the matched elements
  634. for (const element of elements) {
  635. const href = element.getAttribute('href');
  636. const rubyElements = element.querySelectorAll('ruby');
  637.  
  638. // Match the href to extract kanji or vocabulary (ignoring ID if present)
  639. const match = href.match(/\/(kanji|vocabulary)\/(?:\d+\/)?([^\#]*)#/);
  640.  
  641. // If ruby elements exist, extract vocab and reading
  642. if (rubyElements.length > 0) {
  643. let vocabulary = "";
  644. const reading = Array.from(rubyElements)
  645. .map(ruby => {
  646. const rtElement = ruby.querySelector('rt');
  647. vocabulary = vocabulary + (ruby.childNodes[0] ? ruby.childNodes[0].textContent.trim() : '');
  648.  
  649. if (rtElement) {
  650. return rtElement.textContent.trim();
  651. } else {
  652. return ruby.textContent.trim();
  653. }
  654. })
  655. .join('');
  656.  
  657. return [vocabulary, reading];
  658. }
  659.  
  660. // If match exists in href, return that as both vocab and reading
  661. if (match) {
  662. const vocab = match[2].trim();
  663. return [vocab, vocab];
  664. }
  665.  
  666. // Return text content as both vocab and reading if nothing else found
  667. const text = element.textContent.trim();
  668. if (text) {
  669. return [text, text];
  670. }
  671. }
  672. return ['', ''];
  673. }
  674.  
  675. function parseVocabFromReview() {
  676. console.log("Parsing Review Page");
  677.  
  678. // Select the element with class 'kind' to determine the type of content
  679. const kindElement = document.querySelector('.kind');
  680.  
  681. // If kindElement doesn't exist, set kindText to null
  682. const kindText = kindElement ? kindElement.textContent.trim() : null;
  683.  
  684. // Accept 'Kanji' or 'Vocabulary' kindText
  685. if (kindText !== 'Kanji' && kindText !== 'Vocabulary') {
  686. console.log("Not Kanji or existing Vocabulary. Attempting to parse New Vocab.");
  687.  
  688. // Attempt to parse from <a> tag with specific pattern
  689. const anchorElement = document.querySelector('a.plain[href*="/vocabulary/"]');
  690.  
  691. if (anchorElement) {
  692. const href = anchorElement.getAttribute('href');
  693.  
  694. const match = href.match(/\/vocabulary\/\d+\/([^#]+)#a/);
  695.  
  696. if (match && match[1]) {
  697. const new_vocab = match[1];
  698. console.log("Found New Vocab:", new_vocab);
  699. return new_vocab;
  700. }
  701. }
  702.  
  703. console.log("No Vocabulary found.");
  704. return '';
  705. }
  706.  
  707. if (kindText === 'Vocabulary') {
  708. // Select the element with class 'plain' to extract vocabulary
  709. const plainElement = document.querySelector('.plain');
  710. if (!plainElement) {
  711. return '';
  712. }
  713. const rubyElements = plainElement.querySelectorAll('ruby');
  714. // if not ruby elements, return the text content of the plain element twice (katakana word)
  715. if (rubyElements.length === 0) {
  716. const vocabText = plainElement.textContent.trim();
  717. console.log("Found Vocabulary:", vocabText);
  718. return [vocabText, vocabText];
  719. }
  720. // Extract the text from <rt> children and join them.
  721. let vocabulary = "";
  722.  
  723.  
  724. const reading = Array.from(rubyElements)
  725. .map(ruby => {
  726. const rtElement = ruby.querySelector('rt');
  727. // add the text not in the <rt> tag to the vocabulary
  728. vocabulary = vocabulary + (ruby.childNodes[0] ? ruby.childNodes[0].textContent.trim() : '');
  729.  
  730. if (rtElement) {
  731. rtElement.style.display = 'none';
  732. return rtElement.textContent.trim();
  733. } else {
  734. return ruby.textContent.trim();
  735. }
  736. })
  737. .join('');
  738.  
  739. // Regular expression to check if the vocabulary contains kanji characters
  740. const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
  741. if (kanjiRegex.test(vocabulary) || vocabulary) {
  742. console.log("Found Vocabulary:", vocabulary);
  743. return [vocabulary, reading];
  744. }
  745. } else if (kindText === 'Kanji') {
  746. // Select the hidden input element to extract kanji
  747. const hiddenInput = document.querySelector('input[name="c"]');
  748. if (!hiddenInput) {
  749. return '';
  750. }
  751.  
  752. const vocab = hiddenInput.value.split(',')[1];
  753. const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
  754. if (kanjiRegex.test(vocab)) {
  755. console.log("Found Kanji:", vocab);
  756. return vocab;
  757. }
  758. }
  759.  
  760. console.log("No Vocabulary or Kanji found.");
  761. return '';
  762. }
  763.  
  764. function parseVocabFromVocabulary() {
  765. // Get the current URL
  766. let url = window.location.href;
  767.  
  768. // Remove query parameters (e.g., ?lang=english) and fragment identifiers (#)
  769. url = url.split('?')[0].split('#')[0];
  770.  
  771. // Match the URL structure for a vocabulary page
  772. const match = url.match(/https:\/\/jpdb\.io\/vocabulary\/(\d+)\/([^\#\/]*)\/([^\#\/]*)/);
  773. console.log("Parsing Vocabulary Page");
  774.  
  775. if (match) {
  776. // Extract and decode the vocabulary part from the URL
  777. let vocab = match[2];
  778. state.embedAboveSubsectionMeanings = true; // Set state flag
  779. let reading = match[3];
  780. return [decodeURIComponent(vocab), decodeURIComponent(reading)];
  781. }
  782.  
  783. // Return empty string if no match
  784. return '';
  785. }
  786.  
  787. function parseVocabFromKanji() {
  788. // Get the current URL
  789. const url = window.location.href;
  790.  
  791. // Match the URL structure for a kanji page
  792. const match = url.match(/https:\/\/jpdb\.io\/kanji\/(\d+)\/([^\#]*)#a/);
  793. console.log("Parsing Kanji Page");
  794.  
  795. if (match) {
  796. // Extract and decode the kanji part from the URL
  797. let kanji = match[2];
  798. state.embedAboveSubsectionMeanings = true; // Set state flag
  799. kanji = kanji.split('/')[0];
  800. return decodeURIComponent(kanji);
  801. }
  802.  
  803. // Return empty string if no match
  804. return '';
  805. }
  806.  
  807. function parseVocabFromSearch() {
  808. // Get the current URL
  809. let url = window.location.href;
  810.  
  811. // Match the URL structure for a search query, capturing the vocab between `?q=` and either `&` or `+`
  812. const match = url.match(/https:\/\/jpdb\.io\/search\?q=([^&+]*)/);
  813. console.log("Parsing Search Page");
  814.  
  815. if (match) {
  816. // Extract and decode the vocabulary part from the URL
  817. let vocab = match[1];
  818. return decodeURIComponent(vocab);
  819. }
  820.  
  821. // Return empty string if no match
  822. return '';
  823. }
  824.  
  825.  
  826. //EMBED FUNCTIONS=====================================================================================================================
  827. function createAnchor(marginLeft) {
  828. // Create and style an anchor element
  829. const anchor = document.createElement('a');
  830. anchor.href = '#';
  831. anchor.style.border = '0';
  832. anchor.style.display = 'inline-flex';
  833. anchor.style.verticalAlign = 'middle';
  834. anchor.style.marginLeft = marginLeft;
  835. return anchor;
  836. }
  837.  
  838. function createIcon(iconClass, fontSize = '1.4rem', color = '#3d81ff') {
  839. // Create and style an icon element
  840. const icon = document.createElement('i');
  841. icon.className = iconClass;
  842. icon.style.fontSize = fontSize;
  843. icon.style.opacity = '1.0';
  844. icon.style.verticalAlign = 'baseline';
  845. icon.style.color = color;
  846. return icon;
  847. }
  848.  
  849. function createSpeakerButton(soundUrl) {
  850. // Create a speaker button with an icon and click event for audio playback
  851. const anchor = createAnchor('0.5rem');
  852. const icon = createIcon('ti ti-volume');
  853. anchor.appendChild(icon);
  854. anchor.addEventListener('click', (event) => {
  855. event.preventDefault();
  856. playAudio(soundUrl);
  857. });
  858. return anchor;
  859. }
  860.  
  861. function createStarButton() {
  862. // Create a star button with an icon and click event for toggling favorite state
  863. const anchor = createAnchor('0.5rem');
  864. const starIcon = document.createElement('span');
  865. const storedValue = getItem(state.vocab);
  866. // console.log(storedValue);
  867.  
  868. // Determine the star icon (filled or empty) based on stored value
  869. if (storedValue) {
  870. const [storedIndex, storedExactState] = storedValue.split(',');
  871. const index = parseInt(storedIndex, 10);
  872. const exactState = Boolean(parseInt(storedExactState, 10));
  873. starIcon.textContent = (state.currentExampleIndex === index && state.exactSearch === exactState) ? '★' : '☆';
  874. } else {
  875. starIcon.textContent = '☆';
  876. }
  877.  
  878.  
  879. // Style the star icon
  880. starIcon.style.fontSize = '1.4rem';
  881. starIcon.style.color = '#3D8DFF';
  882. starIcon.style.verticalAlign = 'middle';
  883. starIcon.style.position = 'relative';
  884. starIcon.style.top = '-2px';
  885.  
  886. // Append the star icon to the anchor and set up the click event to toggle star state
  887. anchor.appendChild(starIcon);
  888. anchor.addEventListener('click', (event) => {
  889. event.preventDefault();
  890. toggleStarState(starIcon);
  891. });
  892.  
  893. return anchor;
  894. }
  895.  
  896. function toggleStarState(starIcon) {
  897. const storedValue = getItem(state.vocab);
  898. const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
  899.  
  900. // Return early if blacklisted
  901. if (isBlacklisted) {
  902. starIcon.textContent = '☆';
  903. return;
  904. }
  905.  
  906. // Toggle the star state between filled and empty
  907. if (storedValue) {
  908. const [storedIndex, storedExactState] = storedValue.split(',');
  909. const index = parseInt(storedIndex, 10);
  910. const exactState = storedExactState === '1';
  911. if (index === state.currentExampleIndex && exactState === state.exactSearch) {
  912. removeItem(state.vocab);
  913. starIcon.textContent = '☆';
  914. } else {
  915. setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`);
  916. starIcon.textContent = '★';
  917. }
  918. } else {
  919. setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`);
  920. starIcon.textContent = '★';
  921. }
  922. }
  923.  
  924. function createQuoteButton() {
  925. // Create a quote button with an icon and click event for toggling quote style
  926. const anchor = createAnchor('0rem');
  927. const quoteIcon = document.createElement('span');
  928.  
  929. // Set the icon based on exact search state
  930. quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』';
  931.  
  932. // Style the quote icon
  933. quoteIcon.style.fontSize = '1.1rem';
  934. quoteIcon.style.color = '#3D8DFF';
  935. quoteIcon.style.verticalAlign = 'middle';
  936. quoteIcon.style.position = 'relative';
  937. quoteIcon.style.top = '0px';
  938.  
  939. // Append the quote icon to the anchor and set up the click event to toggle quote state
  940. anchor.appendChild(quoteIcon);
  941. anchor.addEventListener('click', (event) => {
  942. event.preventDefault();
  943. toggleQuoteState(quoteIcon);
  944. });
  945.  
  946. return anchor;
  947. }
  948.  
  949. function toggleQuoteState(quoteIcon) {
  950. const storedValue = getItem(state.vocab);
  951. const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
  952.  
  953. // Return early if blacklisted
  954. if (isBlacklisted) {
  955. return;
  956. }
  957.  
  958. // Toggle between single and double quote styles
  959. state.exactSearch = !state.exactSearch;
  960. quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』';
  961.  
  962. // Update state based on stored data
  963. const storedData = getStoredData(state.vocab);
  964. if (storedData && storedData.exactState === state.exactSearch) {
  965. state.currentExampleIndex = storedData.index;
  966. } else {
  967. state.currentExampleIndex = 0;
  968. }
  969.  
  970. state.apiDataFetched = false;
  971. embedImageAndPlayAudio();
  972. getNadeshikoData(state.vocab, state.exactSearch)
  973. .then(() => {
  974. embedImageAndPlayAudio();
  975. })
  976. .catch(error => {
  977. console.error(error);
  978. });
  979. }
  980.  
  981. function createMenuButton() {
  982. // Create a menu button with a dropdown menu
  983. const anchor = createAnchor('0.5rem');
  984. const menuIcon = document.createElement('span');
  985. menuIcon.innerHTML = '☰';
  986.  
  987. // Style the menu icon
  988. menuIcon.style.fontSize = '1.4rem';
  989. menuIcon.style.color = '#3D8DFF';
  990. menuIcon.style.verticalAlign = 'middle';
  991. menuIcon.style.position = 'relative';
  992. menuIcon.style.top = '-2px';
  993.  
  994. // Append the menu icon to the anchor and set up the click event to show the overlay menu
  995. anchor.appendChild(menuIcon);
  996. anchor.addEventListener('click', (event) => {
  997. event.preventDefault();
  998. const overlay = createOverlayMenu();
  999. document.body.appendChild(overlay);
  1000. });
  1001.  
  1002. return anchor;
  1003. }
  1004.  
  1005. function createTextButton(vocab, exact) {
  1006. // Create a text button for Nadeshiko
  1007. const textButton = document.createElement('a');
  1008. textButton.textContent = 'Nadeshiko';
  1009. textButton.style.color = 'var(--subsection-label-color)';
  1010. textButton.style.fontSize = '85%';
  1011. textButton.style.marginRight = '0.5rem';
  1012. textButton.style.verticalAlign = 'middle';
  1013. textButton.href = `https://nadeshiko.co/search/sentence?query=${encodeURIComponent(vocab)}`;
  1014. textButton.target = '_blank';
  1015. return textButton;
  1016. }
  1017.  
  1018. function createButtonContainer(soundUrl, vocab, exact) {
  1019. // Create a container for all buttons
  1020. const buttonContainer = document.createElement('div');
  1021. buttonContainer.className = 'button-container';
  1022. buttonContainer.style.display = 'flex';
  1023. buttonContainer.style.justifyContent = 'space-between';
  1024. buttonContainer.style.alignItems = 'center';
  1025. buttonContainer.style.marginBottom = '5px';
  1026. buttonContainer.style.lineHeight = '1.4rem';
  1027.  
  1028. // Create individual buttons
  1029. const menuButton = createMenuButton();
  1030. const textButton = createTextButton(vocab, exact);
  1031. const speakerButton = createSpeakerButton(soundUrl);
  1032. const starButton = createStarButton();
  1033. const quoteButton = createQuoteButton();
  1034.  
  1035. // Center the buttons within the container
  1036. const centeredButtonsWrapper = document.createElement('div');
  1037. centeredButtonsWrapper.style.display = 'flex';
  1038. centeredButtonsWrapper.style.justifyContent = 'center';
  1039. centeredButtonsWrapper.style.flex = '1';
  1040.  
  1041. centeredButtonsWrapper.append(textButton, speakerButton, starButton, quoteButton);
  1042. buttonContainer.append(centeredButtonsWrapper, menuButton);
  1043.  
  1044. return buttonContainer;
  1045. }
  1046.  
  1047. function stopCurrentAudio() {
  1048. // Stop any currently playing audio
  1049. if (state.currentAudio) {
  1050. state.currentAudio.source.stop();
  1051. state.currentAudio.context.close();
  1052. state.currentAudio = null;
  1053. }
  1054. }
  1055.  
  1056. function playAudio(soundUrl) {
  1057. // Skip playing audio if it is already playing
  1058. if (state.currentlyPlayingAudio) {
  1059. //console.log('Duplicate audio was skipped.');
  1060. return;
  1061. }
  1062.  
  1063. if (soundUrl) {
  1064. state.currentlyPlayingAudio = true;
  1065. stopCurrentAudio();
  1066.  
  1067. GM_xmlhttpRequest({
  1068. method: 'GET',
  1069. url: soundUrl,
  1070. responseType: 'arraybuffer',
  1071. onload: function (response) {
  1072. const AudioContext = window.AudioContext || window.webkitAudioContext;
  1073. const audioContext = new AudioContext();
  1074. audioContext.decodeAudioData(response.response, function (buffer) {
  1075. const source = audioContext.createBufferSource();
  1076. source.buffer = buffer;
  1077.  
  1078. const gainNode = audioContext.createGain();
  1079.  
  1080. // Connect the source to the gain node and the gain node to the destination
  1081. source.connect(gainNode);
  1082. gainNode.connect(audioContext.destination);
  1083.  
  1084. // Mute the first part and then ramp up the volume
  1085. gainNode.gain.setValueAtTime(0, audioContext.currentTime);
  1086. gainNode.gain.linearRampToValueAtTime(CONFIG.SOUND_VOLUME / 100, audioContext.currentTime + 0.1);
  1087.  
  1088. // Play the audio, skip the first part to avoid any "pop"
  1089. source.start(0, 0.05);
  1090.  
  1091. // Log when the audio starts playing
  1092. //console.log('Audio has started playing.');
  1093.  
  1094. // Save the current audio context and source for stopping later
  1095. state.currentAudio = {
  1096. context: audioContext,
  1097. source: source
  1098. };
  1099.  
  1100. // Set currentlyPlayingAudio to false when the audio ends
  1101. source.onended = function () {
  1102. state.currentlyPlayingAudio = false;
  1103. };
  1104. }, function (error) {
  1105. console.error('Error decoding audio:', error);
  1106. state.currentlyPlayingAudio = false;
  1107. });
  1108. },
  1109. onerror: function (error) {
  1110. console.error('Error fetching audio:', error);
  1111. state.currentlyPlayingAudio = false;
  1112. }
  1113. });
  1114. }
  1115. }
  1116.  
  1117. // has to be declared (referenced in multiple functions but definition requires variables local to one function)
  1118. let hotkeysListener;
  1119.  
  1120. function renderImageAndPlayAudio(vocab, shouldAutoPlaySound) {
  1121. if (state.apiDataFetched === false) {
  1122. console.log("No data");
  1123. return;
  1124. }
  1125. const example = state.examples[state.currentExampleIndex] || {};
  1126. const imageUrl = example.media_info?.path_image || null;
  1127. const soundUrl = example.media_info?.path_audio || null;
  1128. const sentence = example.segment_info?.content_jp || null;
  1129. const translation = example.segment_info?.content_en || "";
  1130. const deck_name = example.basic_info?.name_anime_romaji || "Unknown Anime";
  1131. const storedValue = getItem(state.vocab);
  1132. const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
  1133. // Update sentence class content with actual sentence text
  1134. const sentenceElement = document.querySelector('.sentence');
  1135. if (sentenceElement) {
  1136. sentenceElement.textContent = sentence;
  1137. // Update translation class content with actual translation text
  1138. const translationElement = document.querySelector('.sentence-translation');
  1139. if (translationElement) {
  1140. translationElement.textContent = translation;
  1141. } else {
  1142. // get div above .card-sentence
  1143. const divAbove = sentenceElement.parentElement.previousElementSibling;
  1144. if (divAbove) {
  1145. const translationDiv = document.createElement('div');
  1146. translationDiv.style.display = 'flex';
  1147. translationDiv.style.justifyContent = 'center';
  1148. translationDiv.innerHTML = `<div class="sentence-translation blur" style="" onclick="this.classList.remove('blur');" onmouseover="this.classList.remove('blur');">${translation}</div>`;
  1149. divAbove.appendChild(translationDiv);
  1150. }
  1151. }
  1152. } else {
  1153. const answerBox = document.querySelector('.answer-box');
  1154. if (answerBox) {
  1155. // create div style="display: flex; justify-content: center;"
  1156. const sentenceDiv = document.createElement('div');
  1157. sentenceDiv.style.display = 'flex';
  1158. sentenceDiv.style.justifyContent = 'center';
  1159. sentenceDiv.innerHTML = `<div style="display: flex;"><div style="display: flex; flex-direction: column;"><div style="display: flex; align-items: baseline; column-gap: 0.25rem;" class="card-sentence"><div class="sentence" style="margin-left: 0.3rem;">${sentence}</div><a class="icon-link" href="/edit-shown-sentence?v=1168870&amp;s=3448502455&amp;r=1858493110&amp;origin=%2Freview%3Fc%3Dvf%2C1168870%2C3448502455%26r%3D2"><i class="ti ti-pencil"></i></a></div><div style="display: flex;justify-content: center;"><div class="sentence-translation blur" style="" onclick="this.classList.remove('blur');" onmouseover="this.classList.remove('blur');">${translation}</div></div></div></div>`;
  1160. answerBox.appendChild(sentenceDiv);
  1161.  
  1162. }
  1163. }
  1164.  
  1165. // Remove any existing container
  1166. removeExistingContainer();
  1167. if (!shouldRenderContainer()) {
  1168. return;
  1169. }
  1170.  
  1171. // Create and append the main wrapper and text button container
  1172. const wrapperDiv = createWrapperDiv();
  1173. const textDiv = createButtonContainer(soundUrl, vocab, state.exactSearch);
  1174. wrapperDiv.appendChild(textDiv);
  1175.  
  1176.  
  1177. const createTextElement = (text) => {
  1178. const textElement = document.createElement('div');
  1179. textElement.textContent = text;
  1180. textElement.style.padding = '100px 0';
  1181. textElement.style.whiteSpace = 'pre'; // Ensures newlines are respected
  1182. return textElement;
  1183. };
  1184.  
  1185. if (isBlacklisted) {
  1186. wrapperDiv.appendChild(createTextElement('BLACKLISTED'));
  1187. shouldAutoPlaySound = false;
  1188. } else if (state.apiDataFetched) {
  1189. if (imageUrl) {
  1190. const imageElement = createImageElement(wrapperDiv, imageUrl, vocab, state.exactSearch);
  1191. if (imageElement) {
  1192. imageElement.addEventListener('click', () => playAudio(soundUrl));
  1193. }
  1194. } else {
  1195. wrapperDiv.appendChild(createTextElement(`NO IMAGE\n(${deck_name})`));
  1196. }
  1197. // Append sentence and translation or a placeholder text
  1198. // sentence ? appendSentenceAndTranslation(wrapperDiv, sentence, translation) : appendNoneText(wrapperDiv);
  1199. } else if (!sentence) {
  1200. wrapperDiv.appendChild(createTextElement('ERROR\nNO EXAMPLES FOUND\n\nRARE WORD OR NADESHIKO API IS TEMPORARILY DOWN'));
  1201. } else {
  1202. wrapperDiv.appendChild(createTextElement('LOADING'));
  1203. }
  1204.  
  1205.  
  1206. // Create navigation elements
  1207. const navigationDiv = createNavigationDiv();
  1208. const leftArrow = createLeftArrow(vocab, shouldAutoPlaySound);
  1209. const rightArrow = createRightArrow(vocab, shouldAutoPlaySound);
  1210.  
  1211. // Create and append the main container
  1212. const containerDiv = createContainerDiv(leftArrow, wrapperDiv, rightArrow, navigationDiv);
  1213. appendContainer(containerDiv);
  1214.  
  1215. // Auto-play sound if configured
  1216. if (CONFIG.AUTO_PLAY_SOUND && shouldAutoPlaySound) {
  1217. playAudio(soundUrl);
  1218. }
  1219.  
  1220. // Link hotkeys
  1221. if (CONFIG.HOTKEYS.indexOf("None") === -1) {
  1222. const leftHotkey = CONFIG.HOTKEYS[0];
  1223. const rightHotkey = CONFIG.HOTKEYS[1];
  1224.  
  1225. hotkeysListener = (event) => {
  1226. if (event.repeat) {
  1227. return;
  1228. }
  1229. switch (event.key.toLowerCase()) {
  1230. case leftHotkey.toLowerCase():
  1231. if (leftArrow.disabled) {
  1232. // listener gets removed, so need to re-add
  1233. window.addEventListener('keydown', hotkeysListener, {once: true});
  1234. } else {
  1235. leftArrow.click(); // don't need to re-add listener because renderImageAndPlayAudio() will run again
  1236. }
  1237. break;
  1238. case rightHotkey.toLowerCase():
  1239. if (rightArrow.disabled) {
  1240. // listener gets removed, so need to re-add
  1241. window.addEventListener('keydown', hotkeysListener, {once: true});
  1242. } else {
  1243. rightArrow.click(); // don't need to re-add listener because renderImageAndPlayAudio() will run again
  1244. }
  1245. break;
  1246. default:
  1247. // listener gets removed, so need to re-add
  1248. window.addEventListener('keydown', hotkeysListener, {once: true});
  1249. }
  1250. };
  1251.  
  1252. window.addEventListener('keydown', hotkeysListener, {once: true});
  1253. }
  1254. }
  1255.  
  1256. function removeExistingContainer() {
  1257. // Remove the existing container if it exists
  1258. const existingContainer = document.getElementById('nadeshiko-container');
  1259. if (existingContainer) {
  1260. existingContainer.remove();
  1261. }
  1262. window.removeEventListener('keydown', hotkeysListener);
  1263. }
  1264.  
  1265. function shouldRenderContainer() {
  1266. // Determine if the container should be rendered based on the presence of certain elements
  1267. const resultVocabularySection = document.querySelector('.result.vocabulary');
  1268. const hboxWrapSection = document.querySelector('.hbox.wrap');
  1269. const subsectionMeanings = document.querySelector('.subsection-meanings');
  1270. const subsectionLabels = document.querySelectorAll('h6.subsection-label');
  1271. return resultVocabularySection || hboxWrapSection || subsectionMeanings || subsectionLabels.length >= 3;
  1272. }
  1273.  
  1274. function createWrapperDiv() {
  1275. // Create and style the wrapper div
  1276. const wrapperDiv = document.createElement('div');
  1277. wrapperDiv.id = 'image-wrapper';
  1278. wrapperDiv.style.textAlign = 'center';
  1279. wrapperDiv.style.padding = '5px 0';
  1280. return wrapperDiv;
  1281. }
  1282.  
  1283. function createImageElement(wrapperDiv, imageUrl, vocab, exactSearch) {
  1284. // Create and return an image element with specified attributes
  1285. const searchVocab = exactSearch ? `「${vocab}」` : vocab;
  1286. const example = state.examples[state.currentExampleIndex] || {};
  1287. const deck_name = example.basic_info.name_anime_romaji || null;
  1288.  
  1289. // Extract the file name from the URL
  1290. let file_name = imageUrl.substring(imageUrl.lastIndexOf('/') + 1);
  1291.  
  1292. // Remove prefixes "Anime_", "A_", or "Z" from the file name
  1293. file_name = file_name.replace(/^(Anime_|A_|Z)/, '');
  1294.  
  1295. const titleText = `${searchVocab} #${state.currentExampleIndex + 1} \n${deck_name} \n${file_name}`;
  1296.  
  1297. return GM_addElement(wrapperDiv, 'img', {
  1298. src: imageUrl,
  1299. alt: 'Embedded Image',
  1300. title: titleText,
  1301. style: `max-width: ${CONFIG.IMAGE_WIDTH}; margin-top: 10px; cursor: pointer;`
  1302. });
  1303. }
  1304.  
  1305. function highlightVocab(sentence, vocab) {
  1306. // Highlight vocabulary in the sentence based on configuration
  1307. if (!CONFIG.COLORED_SENTENCE_TEXT) {
  1308. return sentence;
  1309. }
  1310.  
  1311. if (state.exactSearch) {
  1312. const regex = new RegExp(`(${vocab})`, 'g');
  1313. return sentence.replace(regex, '<span style="color: var(--outline-input-color);">$1</span>');
  1314. } else {
  1315. return vocab.split('').reduce((acc, char) => {
  1316. const regex = new RegExp(char, 'g');
  1317. return acc.replace(regex, `<span style="color: var(--outline-input-color);">${char}</span>`);
  1318. }, sentence);
  1319. }
  1320. }
  1321.  
  1322. function appendSentenceAndTranslation(wrapperDiv, sentence, translation) {
  1323. // Append sentence and translation to the wrapper div
  1324. const sentenceText = document.createElement('div');
  1325. sentenceText.innerHTML = highlightVocab(sentence, state.vocab);
  1326. sentenceText.style.marginTop = '10px';
  1327. sentenceText.style.fontSize = CONFIG.SENTENCE_FONT_SIZE;
  1328. sentenceText.style.color = 'lightgray';
  1329. sentenceText.style.maxWidth = CONFIG.IMAGE_WIDTH;
  1330. sentenceText.style.whiteSpace = 'pre-wrap';
  1331. wrapperDiv.appendChild(sentenceText);
  1332.  
  1333. if (CONFIG.ENABLE_EXAMPLE_TRANSLATION && translation) {
  1334. const translationText = document.createElement('div');
  1335. translationText.innerHTML = replaceSpecialCharacters(translation);
  1336. translationText.style.marginTop = '5px';
  1337. translationText.style.fontSize = CONFIG.TRANSLATION_FONT_SIZE;
  1338. translationText.style.color = 'var(--subsection-label-color)';
  1339. translationText.style.maxWidth = CONFIG.IMAGE_WIDTH;
  1340. translationText.style.whiteSpace = 'pre-wrap';
  1341. wrapperDiv.appendChild(translationText);
  1342. }
  1343. }
  1344.  
  1345. function appendNoneText(wrapperDiv) {
  1346. // Append a "None" text to the wrapper div
  1347. const noneText = document.createElement('div');
  1348. noneText.textContent = 'None';
  1349. noneText.style.marginTop = '10px';
  1350. noneText.style.fontSize = '85%';
  1351. noneText.style.color = 'var(--subsection-label-color)';
  1352. wrapperDiv.appendChild(noneText);
  1353. }
  1354.  
  1355. function createNavigationDiv() {
  1356. // Create and style the navigation div
  1357. const navigationDiv = document.createElement('div');
  1358. navigationDiv.id = 'nadeshiko-embed';
  1359. navigationDiv.style.display = 'flex';
  1360. navigationDiv.style.justifyContent = 'center';
  1361. navigationDiv.style.alignItems = 'center';
  1362. navigationDiv.style.maxWidth = CONFIG.IMAGE_WIDTH;
  1363. navigationDiv.style.margin = '0 auto';
  1364. return navigationDiv;
  1365. }
  1366.  
  1367. function createLeftArrow(vocab, shouldAutoPlaySound) {
  1368. // Create and configure the left arrow button
  1369. const leftArrow = document.createElement('button');
  1370. leftArrow.textContent = '<';
  1371. leftArrow.style.marginRight = '10px';
  1372. leftArrow.style.width = CONFIG.ARROW_WIDTH;
  1373. leftArrow.style.height = CONFIG.ARROW_HEIGHT;
  1374. leftArrow.style.lineHeight = '25px';
  1375. leftArrow.style.textAlign = 'center';
  1376. leftArrow.style.display = 'flex';
  1377. leftArrow.style.justifyContent = 'center';
  1378. leftArrow.style.alignItems = 'center';
  1379. leftArrow.style.padding = '0'; // Remove padding
  1380. leftArrow.disabled = state.currentExampleIndex === 0;
  1381. leftArrow.addEventListener('click', () => {
  1382. if (state.currentExampleIndex > 0) {
  1383. state.currentExampleIndex--;
  1384. state.currentlyPlayingAudio = false;
  1385. renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
  1386. preloadImages();
  1387. }
  1388. });
  1389. return leftArrow;
  1390. }
  1391.  
  1392. function createRightArrow(vocab, shouldAutoPlaySound) {
  1393. // Create and configure the right arrow button
  1394. const rightArrow = document.createElement('button');
  1395. rightArrow.textContent = '>';
  1396. rightArrow.style.marginLeft = '10px';
  1397. rightArrow.style.width = CONFIG.ARROW_WIDTH;
  1398. rightArrow.style.height = CONFIG.ARROW_HEIGHT;
  1399. rightArrow.style.lineHeight = '25px';
  1400. rightArrow.style.textAlign = 'center';
  1401. rightArrow.style.display = 'flex';
  1402. rightArrow.style.justifyContent = 'center';
  1403. rightArrow.style.alignItems = 'center';
  1404. rightArrow.style.padding = '0'; // Remove padding
  1405. rightArrow.disabled = state.currentExampleIndex >= state.examples.length - 1;
  1406. rightArrow.addEventListener('click', () => {
  1407. if (state.currentExampleIndex < state.examples.length - 1) {
  1408. state.currentExampleIndex++;
  1409. state.currentlyPlayingAudio = false;
  1410. renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
  1411. preloadImages();
  1412. }
  1413. });
  1414. return rightArrow;
  1415. }
  1416.  
  1417. function createContainerDiv(leftArrow, wrapperDiv, rightArrow, navigationDiv) {
  1418. // Create and configure the main container div
  1419. const containerDiv = document.createElement('div');
  1420. containerDiv.id = 'nadeshiko-container';
  1421. containerDiv.style.display = 'flex';
  1422. containerDiv.style.alignItems = 'center';
  1423. containerDiv.style.justifyContent = 'center';
  1424. containerDiv.style.flexDirection = 'column';
  1425.  
  1426. const arrowWrapperDiv = document.createElement('div');
  1427. arrowWrapperDiv.style.display = 'flex';
  1428. arrowWrapperDiv.style.alignItems = 'center';
  1429. arrowWrapperDiv.style.justifyContent = 'center';
  1430.  
  1431. arrowWrapperDiv.append(leftArrow, wrapperDiv, rightArrow);
  1432. containerDiv.append(arrowWrapperDiv, navigationDiv);
  1433.  
  1434. return containerDiv;
  1435. }
  1436.  
  1437. function appendContainer(containerDiv) {
  1438. // Append the container div to the appropriate section based on configuration
  1439. const resultVocabularySection = document.querySelector('.result.vocabulary');
  1440. const hboxWrapSection = document.querySelector('.hbox.wrap');
  1441. const subsectionMeanings = document.querySelector('.subsection-meanings');
  1442. const subsectionComposedOfKanji = document.querySelector('.subsection-composed-of-kanji');
  1443. const subsectionPitchAccent = document.querySelector('.subsection-pitch-accent');
  1444. const subsectionLabels = document.querySelectorAll('h6.subsection-label');
  1445. const vboxGap = document.querySelector('.vbox.gap');
  1446. const styleSheet = document.querySelector('link[rel="stylesheet"]').sheet;
  1447.  
  1448. if (CONFIG.WIDE_MODE && subsectionMeanings) {
  1449. const wrapper = document.createElement('div');
  1450. wrapper.style.display = 'flex';
  1451. wrapper.style.alignItems = 'flex-start';
  1452. styleSheet.insertRule('.subsection-meanings { max-width: none !important; }', styleSheet.cssRules.length);
  1453.  
  1454. const originalContentWrapper = document.createElement('div');
  1455. originalContentWrapper.style.flex = '1';
  1456. originalContentWrapper.appendChild(subsectionMeanings);
  1457.  
  1458. if (subsectionComposedOfKanji) {
  1459. const newline1 = document.createElement('br');
  1460. originalContentWrapper.appendChild(newline1);
  1461. originalContentWrapper.appendChild(subsectionComposedOfKanji);
  1462. }
  1463. if (subsectionPitchAccent) {
  1464. const newline2 = document.createElement('br');
  1465. originalContentWrapper.appendChild(newline2);
  1466. originalContentWrapper.appendChild(subsectionPitchAccent);
  1467. }
  1468.  
  1469. if (CONFIG.DEFINITIONS_ON_RIGHT_IN_WIDE_MODE) {
  1470. wrapper.appendChild(containerDiv);
  1471. wrapper.appendChild(originalContentWrapper);
  1472. } else {
  1473. wrapper.appendChild(originalContentWrapper);
  1474. wrapper.appendChild(containerDiv);
  1475. }
  1476.  
  1477. if (vboxGap) {
  1478. const existingDynamicDiv = vboxGap.querySelector('#dynamic-content');
  1479. if (existingDynamicDiv) {
  1480. existingDynamicDiv.remove();
  1481. }
  1482.  
  1483. const dynamicDiv = document.createElement('div');
  1484. dynamicDiv.id = 'dynamic-content';
  1485. dynamicDiv.appendChild(wrapper);
  1486.  
  1487. if (window.location.href.includes('vocabulary')) {
  1488. vboxGap.insertBefore(dynamicDiv, vboxGap.children[1]);
  1489. } else {
  1490. vboxGap.insertBefore(dynamicDiv, vboxGap.firstChild);
  1491. }
  1492. }
  1493. } else {
  1494. if (state.embedAboveSubsectionMeanings && subsectionMeanings) {
  1495. subsectionMeanings.parentNode.insertBefore(containerDiv, subsectionMeanings);
  1496. } else if (resultVocabularySection) {
  1497. resultVocabularySection.parentNode.insertBefore(containerDiv, resultVocabularySection);
  1498. } else if (hboxWrapSection) {
  1499. hboxWrapSection.parentNode.insertBefore(containerDiv, hboxWrapSection);
  1500. } else if (subsectionLabels.length >= 4) {
  1501. subsectionLabels[3].parentNode.insertBefore(containerDiv, subsectionLabels[3]);
  1502. }
  1503. }
  1504. }
  1505.  
  1506. function embedImageAndPlayAudio() {
  1507. // Embed the image and play audio, removing existing navigation div if present
  1508. console.log("Embedding image and playing audio");
  1509. const existingNavigationDiv = document.getElementById('nadeshiko-embed');
  1510. if (existingNavigationDiv) {
  1511. existingNavigationDiv.remove();
  1512. }
  1513.  
  1514. const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
  1515.  
  1516. renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
  1517. preloadImages();
  1518. }
  1519.  
  1520. function replaceSpecialCharacters(text) {
  1521. // Replace special characters in the text
  1522. return text.replace(/<br>/g, '\n').replace(/&quot;/g, '"').replace(/\n/g, '<br>');
  1523. }
  1524.  
  1525. function preloadImages() {
  1526. // Preload images around the current example index
  1527. const preloadDiv = GM_addElement(document.body, 'div', {style: 'display: none;'});
  1528. const startIndex = Math.max(0, state.currentExampleIndex - CONFIG.NUMBER_OF_PRELOADS);
  1529. const endIndex = Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUMBER_OF_PRELOADS);
  1530.  
  1531. for (let i = startIndex; i <= endIndex; i++) {
  1532. if (!state.preloadedIndices.has(i) && state.examples[i].image_url) {
  1533. GM_addElement(preloadDiv, 'img', {src: state.examples[i].image_url});
  1534. state.preloadedIndices.add(i);
  1535. }
  1536. }
  1537. }
  1538.  
  1539.  
  1540. //MENU FUNCTIONS=====================================================================================================================
  1541. ////FILE OPERATIONS=====================================================================================================================
  1542. function handleImportButtonClick() {
  1543. handleFileInput('application/json', importFavorites);
  1544. }
  1545.  
  1546. function handleImportDButtonClick() {
  1547. handleFileInput('application/json', importData);
  1548. }
  1549.  
  1550. function handleFileInput(acceptType, callback) {
  1551. const fileInput = document.createElement('input');
  1552. fileInput.type = 'file';
  1553. fileInput.accept = acceptType;
  1554. fileInput.addEventListener('change', callback);
  1555. fileInput.click();
  1556. }
  1557.  
  1558. function createBlobAndDownload(data, filename, type) {
  1559. const blob = new Blob([data], {type});
  1560. const url = URL.createObjectURL(blob);
  1561. const a = document.createElement('a');
  1562. a.href = url;
  1563. a.download = filename;
  1564. document.body.appendChild(a);
  1565. a.click();
  1566. document.body.removeChild(a);
  1567. URL.revokeObjectURL(url);
  1568. }
  1569.  
  1570. function addBlacklist() {
  1571. setItem(state.vocab, `0,2`);
  1572. location.reload();
  1573. }
  1574.  
  1575. function remBlacklist() {
  1576. removeItem(state.vocab);
  1577. location.reload();
  1578. }
  1579.  
  1580. function exportFavorites() {
  1581. const favorites = {};
  1582. for (let i = 0; i < localStorage.length; i++) {
  1583. const key = localStorage.key(i);
  1584. if (key.startsWith(scriptPrefix)) {
  1585. const keyPrefixless = key.substring(scriptPrefix.length); // chop off the script prefix
  1586. if (!keyPrefixless.startsWith(configPrefix)) {
  1587. favorites[keyPrefixless] = localStorage.getItem(key);
  1588. // For backwards compatibility keep the exported keys prefixless
  1589. }
  1590. }
  1591. }
  1592. const data = JSON.stringify(favorites, null, 2);
  1593. createBlobAndDownload(data, 'favorites.json', 'application/json');
  1594. }
  1595.  
  1596. function importFavorites(event) {
  1597. const file = event.target.files[0];
  1598. if (!file) {
  1599. return;
  1600. }
  1601.  
  1602. const reader = new FileReader();
  1603. reader.onload = function (e) {
  1604. try {
  1605. const favorites = JSON.parse(e.target.result);
  1606. for (const key in favorites) {
  1607. setItem(key, favorites[key]);
  1608. }
  1609. alert('Favorites imported successfully!');
  1610. location.reload();
  1611. } catch (error) {
  1612. alert('Error importing favorites:', error);
  1613. }
  1614. };
  1615. reader.readAsText(file);
  1616. }
  1617.  
  1618. async function exportData() {
  1619. const dataEntries = {};
  1620.  
  1621. try {
  1622. const db = await IndexedDBManager.open();
  1623. const indexedDBData = await IndexedDBManager.getAll(db);
  1624. indexedDBData.forEach(item => {
  1625. dataEntries[item.keyword] = item.data;
  1626. });
  1627.  
  1628. const data = JSON.stringify(dataEntries, null, 2);
  1629. createBlobAndDownload(data, 'data.json', 'application/json');
  1630. } catch (error) {
  1631. console.error('Error exporting data from IndexedDB:', error);
  1632. }
  1633. }
  1634.  
  1635. async function importData(event) {
  1636. const file = event.target.files[0];
  1637. if (!file) {
  1638. return;
  1639. }
  1640.  
  1641. const reader = new FileReader();
  1642. reader.onload = async function (e) {
  1643. try {
  1644. const dataEntries = JSON.parse(e.target.result);
  1645.  
  1646. const db = await IndexedDBManager.open();
  1647. for (const key in dataEntries) {
  1648. await IndexedDBManager.save(db, key, dataEntries[key]);
  1649. }
  1650.  
  1651. alert('Data imported successfully!');
  1652. location.reload();
  1653. } catch (error) {
  1654. alert('Error importing data:', error);
  1655. }
  1656. };
  1657. reader.readAsText(file);
  1658. }
  1659.  
  1660.  
  1661. ////CONFIRMATION
  1662. function createConfirmationPopup(messageText, onYes, onNo) {
  1663. // Create a confirmation popup with Yes and No buttons
  1664. const popupOverlay = document.createElement('div');
  1665. popupOverlay.style.position = 'fixed';
  1666. popupOverlay.style.top = '0';
  1667. popupOverlay.style.left = '0';
  1668. popupOverlay.style.width = '100%';
  1669. popupOverlay.style.height = '100%';
  1670. popupOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
  1671. popupOverlay.style.zIndex = '1001';
  1672. popupOverlay.style.display = 'flex';
  1673. popupOverlay.style.justifyContent = 'center';
  1674. popupOverlay.style.alignItems = 'center';
  1675.  
  1676. const popupContent = document.createElement('div');
  1677. popupContent.style.backgroundColor = 'var(--background-color)';
  1678. popupContent.style.padding = '20px';
  1679. popupContent.style.borderRadius = '5px';
  1680. popupContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
  1681. popupContent.style.textAlign = 'center';
  1682.  
  1683. const message = document.createElement('p');
  1684. message.textContent = messageText;
  1685.  
  1686. const yesButton = document.createElement('button');
  1687. yesButton.textContent = 'Yes';
  1688. yesButton.style.backgroundColor = '#C82800';
  1689. yesButton.style.marginRight = '10px';
  1690. yesButton.addEventListener('click', () => {
  1691. onYes();
  1692. document.body.removeChild(popupOverlay);
  1693. });
  1694.  
  1695. const noButton = document.createElement('button');
  1696. noButton.textContent = 'No';
  1697. noButton.addEventListener('click', () => {
  1698. onNo();
  1699. document.body.removeChild(popupOverlay);
  1700. });
  1701.  
  1702. popupContent.appendChild(message);
  1703. popupContent.appendChild(yesButton);
  1704. popupContent.appendChild(noButton);
  1705. popupOverlay.appendChild(popupContent);
  1706.  
  1707. document.body.appendChild(popupOverlay);
  1708. }
  1709.  
  1710. ////BUTTONS
  1711. function createActionButtonsContainer() {
  1712. const actionButtonWidth = '100px';
  1713.  
  1714. const closeButton = createButton('Close', '10px', closeOverlayMenu, actionButtonWidth);
  1715. const saveButton = createButton('Save', '10px', saveConfig, actionButtonWidth);
  1716. const defaultButton = createDefaultButton(actionButtonWidth);
  1717. const deleteButton = createDeleteButton(actionButtonWidth);
  1718.  
  1719. const actionButtonsContainer = document.createElement('div');
  1720. actionButtonsContainer.style.textAlign = 'center';
  1721. actionButtonsContainer.style.marginTop = '10px';
  1722. actionButtonsContainer.append(closeButton, saveButton, defaultButton, deleteButton);
  1723.  
  1724. return actionButtonsContainer;
  1725. }
  1726.  
  1727. function createMenuButtons() {
  1728. const blacklistContainer = createBlacklistContainer();
  1729. const favoritesContainer = createFavoritesContainer();
  1730. const dataContainer = createDataContainer();
  1731. const actionButtonsContainer = createActionButtonsContainer();
  1732.  
  1733. const buttonContainer = document.createElement('div');
  1734. buttonContainer.append(blacklistContainer, favoritesContainer, dataContainer, actionButtonsContainer);
  1735.  
  1736. return buttonContainer;
  1737. }
  1738.  
  1739. function createButton(text, margin, onClick, width) {
  1740. // Create a button element with specified properties
  1741. const button = document.createElement('button');
  1742. button.textContent = text;
  1743. button.style.margin = margin;
  1744. button.style.width = width;
  1745. button.style.textAlign = 'center';
  1746. button.style.display = 'inline-block';
  1747. button.style.lineHeight = '30px';
  1748. button.style.padding = '5px 0';
  1749. button.addEventListener('click', onClick);
  1750. return button;
  1751. }
  1752.  
  1753. ////BLACKLIST BUTTONS
  1754. function createBlacklistContainer() {
  1755. const blacklistButtonWidth = '200px';
  1756.  
  1757. const addBlacklistButton = createButton('Add to Blacklist', '10px', addBlacklist, blacklistButtonWidth);
  1758. const remBlacklistButton = createButton('Remove from Blacklist', '10px', remBlacklist, blacklistButtonWidth);
  1759.  
  1760. const blacklistContainer = document.createElement('div');
  1761. blacklistContainer.style.textAlign = 'center';
  1762. blacklistContainer.style.marginTop = '10px';
  1763. blacklistContainer.append(addBlacklistButton, remBlacklistButton);
  1764.  
  1765. return blacklistContainer;
  1766. }
  1767.  
  1768. ////FAVORITE BUTTONS
  1769. function createFavoritesContainer() {
  1770. const favoritesButtonWidth = '200px';
  1771.  
  1772. const exportButton = createButton('Export Favorites', '10px', exportFavorites, favoritesButtonWidth);
  1773. const importButton = createButton('Import Favorites', '10px', handleImportButtonClick, favoritesButtonWidth);
  1774.  
  1775. const favoritesContainer = document.createElement('div');
  1776. favoritesContainer.style.textAlign = 'center';
  1777. favoritesContainer.style.marginTop = '10px';
  1778. favoritesContainer.append(exportButton, importButton);
  1779.  
  1780. return favoritesContainer;
  1781.  
  1782. }
  1783.  
  1784. ////DATA BUTTONS
  1785. function createDataContainer() {
  1786. const dataButtonWidth = '200px';
  1787.  
  1788. const exportButton = createButton('Export Data', '10px', exportData, dataButtonWidth);
  1789. const importButton = createButton('Import Data', '10px', handleImportDButtonClick, dataButtonWidth);
  1790.  
  1791. const dataContainer = document.createElement('div');
  1792. dataContainer.style.textAlign = 'center';
  1793. dataContainer.style.marginTop = '10px';
  1794. dataContainer.append(exportButton, importButton);
  1795.  
  1796. return dataContainer;
  1797. }
  1798.  
  1799. ////CLOSE BUTTON
  1800. function closeOverlayMenu() {
  1801. loadConfig();
  1802. document.body.removeChild(document.getElementById('overlayMenu'));
  1803. }
  1804.  
  1805. ////SAVE BUTTON
  1806. function saveConfig() {
  1807. const overlay = document.getElementById('overlayMenu');
  1808. if (!overlay) {
  1809. return;
  1810. }
  1811.  
  1812. const inputs = overlay.querySelectorAll('input, span');
  1813. console.log(inputs);
  1814. const {changes, minimumExampleLengthChanged, newMinimumExampleLength} = gatherChanges(inputs);
  1815. if (minimumExampleLengthChanged) {
  1816. handleMinimumExampleLengthChange(newMinimumExampleLength, changes);
  1817. } else {
  1818. applyChanges(changes);
  1819. finalizeSaveConfig();
  1820. setVocabSize();
  1821. setPageWidth();
  1822. }
  1823. }
  1824.  
  1825. function gatherChanges(inputs) {
  1826. let minimumExampleLengthChanged = false;
  1827. let newMinimumExampleLength;
  1828. const changes = {};
  1829.  
  1830. inputs.forEach(input => {
  1831. const key = input.getAttribute('data-key');
  1832. const type = input.getAttribute('data-type');
  1833. let value;
  1834.  
  1835. if (type === 'boolean') {
  1836. value = input.checked;
  1837. } else if (type === 'number') {
  1838. value = parseFloat(input.textContent);
  1839. } else if (type === 'string') {
  1840. value = input.textContent;
  1841. } else if (type === 'object' && key === 'HOTKEYS') {
  1842. value = input.textContent.replace(' and ', ' ');
  1843. }
  1844.  
  1845. if (key && type) {
  1846. const typePart = input.getAttribute('data-type-part');
  1847. const originalFormattedType = typePart.slice(1, -1);
  1848.  
  1849. if (key === 'MINIMUM_EXAMPLE_LENGTH' && CONFIG.MINIMUM_EXAMPLE_LENGTH !== value) {
  1850. minimumExampleLengthChanged = true;
  1851. newMinimumExampleLength = value;
  1852. }
  1853. if (key === 'MAXIMUM_EXAMPLE_LENGTH' && CONFIG.MAXIMUM_EXAMPLE_LENGTH !== value) {
  1854. value = Math.max(value, CONFIG.MINIMUM_EXAMPLE_LENGTH);
  1855. }
  1856. changes[configPrefix + key] = value + originalFormattedType;
  1857. }
  1858. });
  1859.  
  1860. return {changes, minimumExampleLengthChanged, newMinimumExampleLength};
  1861. }
  1862.  
  1863. function handleMinimumExampleLengthChange(newMinimumExampleLength, changes) {
  1864. createConfirmationPopup(
  1865. 'Changing Minimum Example Length will break your current favorites. They will all be deleted. Are you sure?',
  1866. async () => {
  1867. await IndexedDBManager.delete();
  1868. CONFIG.MINIMUM_EXAMPLE_LENGTH = newMinimumExampleLength;
  1869. setItem(`${configPrefix}MINIMUM_EXAMPLE_LENGTH`, newMinimumExampleLength);
  1870. applyChanges(changes);
  1871. clearNonConfigLocalStorage();
  1872. finalizeSaveConfig();
  1873. location.reload();
  1874. },
  1875. () => {
  1876. const overlay = document.getElementById('overlayMenu');
  1877. document.body.removeChild(overlay);
  1878. document.body.appendChild(createOverlayMenu());
  1879. }
  1880. );
  1881. }
  1882.  
  1883. function clearNonConfigLocalStorage() {
  1884. for (let i = 0; i < localStorage.length; i++) {
  1885. const key = localStorage.key(i);
  1886. if (key && key.startsWith(scriptPrefix) && !key.startsWith(scriptPrefix + configPrefix)) {
  1887. localStorage.removeItem(key);
  1888. i--; // Adjust index after removal
  1889. }
  1890. }
  1891. }
  1892.  
  1893. function applyChanges(changes) {
  1894. for (const key in changes) {
  1895. setItem(key, changes[key]);
  1896. }
  1897. }
  1898.  
  1899. function finalizeSaveConfig() {
  1900. loadConfig();
  1901. window.removeEventListener('keydown', hotkeysListener);
  1902. renderImageAndPlayAudio(state.vocab, CONFIG.AUTO_PLAY_SOUND);
  1903. const overlay = document.getElementById('overlayMenu');
  1904. if (overlay) {
  1905. document.body.removeChild(overlay);
  1906. }
  1907. }
  1908.  
  1909.  
  1910. ////DEFAULT BUTTON
  1911. function createDefaultButton(width) {
  1912. const defaultButton = createButton('Default', '10px', () => {
  1913. createConfirmationPopup(
  1914. 'This will reset all your settings to default. Are you sure?',
  1915. () => {
  1916. Object.keys(localStorage).forEach(key => {
  1917. if (key.startsWith(scriptPrefix + configPrefix)) {
  1918. localStorage.removeItem(key);
  1919. }
  1920. });
  1921. location.reload();
  1922. },
  1923. () => {
  1924. const overlay = document.getElementById('overlayMenu');
  1925. if (overlay) {
  1926. document.body.removeChild(overlay);
  1927. }
  1928. loadConfig();
  1929. document.body.appendChild(createOverlayMenu());
  1930. }
  1931. );
  1932. }, width);
  1933. defaultButton.style.backgroundColor = '#C82800';
  1934. defaultButton.style.color = 'white';
  1935. return defaultButton;
  1936. }
  1937.  
  1938.  
  1939. ////DELETE BUTTON
  1940. function createDeleteButton(width) {
  1941. const deleteButton = createButton('DELETE', '10px', () => {
  1942. createConfirmationPopup(
  1943. 'This will delete all your favorites and cached data. Are you sure?',
  1944. async () => {
  1945. await IndexedDBManager.delete();
  1946. Object.keys(localStorage).forEach(key => {
  1947. if (key.startsWith(scriptPrefix) && !key.startsWith(scriptPrefix + configPrefix)) {
  1948. localStorage.removeItem(key);
  1949. }
  1950. });
  1951. location.reload();
  1952. },
  1953. () => {
  1954. const overlay = document.getElementById('overlayMenu');
  1955. if (overlay) {
  1956. document.body.removeChild(overlay);
  1957. }
  1958. loadConfig();
  1959. document.body.appendChild(createOverlayMenu());
  1960. }
  1961. );
  1962. }, width);
  1963. deleteButton.style.backgroundColor = '#C82800';
  1964. deleteButton.style.color = 'white';
  1965. return deleteButton;
  1966. }
  1967.  
  1968. function createOverlayMenu() {
  1969. // Create and return the overlay menu for configuration settings
  1970. const overlay = document.createElement('div');
  1971. overlay.id = 'overlayMenu';
  1972. overlay.style.position = 'fixed';
  1973. overlay.style.top = '0';
  1974. overlay.style.left = '0';
  1975. overlay.style.width = '100%';
  1976. overlay.style.height = '100%';
  1977. overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
  1978. overlay.style.zIndex = '1000';
  1979. overlay.style.display = 'flex';
  1980. overlay.style.justifyContent = 'center';
  1981. overlay.style.alignItems = 'center';
  1982.  
  1983. const menuContent = document.createElement('div');
  1984. menuContent.style.backgroundColor = 'var(--background-color)';
  1985. menuContent.style.color = 'var(--text-color)';
  1986. menuContent.style.padding = '20px';
  1987. menuContent.style.borderRadius = '5px';
  1988. menuContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
  1989. menuContent.style.width = '80%';
  1990. menuContent.style.maxWidth = '550px';
  1991. menuContent.style.maxHeight = '80%';
  1992. menuContent.style.overflowY = 'auto';
  1993.  
  1994. for (const [key, value] of Object.entries(CONFIG)) {
  1995. const optionContainer = document.createElement('div');
  1996. optionContainer.style.marginBottom = '10px';
  1997. optionContainer.style.display = 'flex';
  1998. optionContainer.style.alignItems = 'center';
  1999.  
  2000. const leftContainer = document.createElement('div');
  2001. leftContainer.style.flex = '1';
  2002. leftContainer.style.display = 'flex';
  2003. leftContainer.style.alignItems = 'center';
  2004.  
  2005. const rightContainer = document.createElement('div');
  2006. rightContainer.style.flex = '1';
  2007. rightContainer.style.display = 'flex';
  2008. rightContainer.style.alignItems = 'center';
  2009. rightContainer.style.justifyContent = 'center';
  2010.  
  2011. const label = document.createElement('label');
  2012. label.textContent = key.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
  2013. label.style.marginRight = '10px';
  2014.  
  2015. leftContainer.appendChild(label);
  2016.  
  2017. if (key === 'RANDOM_SENTENCE') {
  2018. const select = document.createElement('select');
  2019. select.setAttribute('data-key', key);
  2020.  
  2021. // Add options to the select dropdown for the enum values
  2022. for (const [enumKey, enumValue] of Object.entries(RANDOM_SENTENCE_ENUM)) {
  2023. const option = document.createElement('option');
  2024. option.value = enumValue;
  2025. option.text = enumKey.replace(/_/g, ' ').toLowerCase();
  2026. option.selected = value === enumValue; // Set the current value as selected
  2027. select.appendChild(option);
  2028. }
  2029.  
  2030. select.addEventListener('change', (event) => {
  2031. CONFIG[key] = parseInt(event.target.value, 10); // Update the config with the selected value
  2032. localStorage.setItem(`${scriptPrefix + configPrefix}${key}`, event.target.value); // Save to localStorage
  2033. });
  2034.  
  2035. rightContainer.appendChild(select);
  2036. } else if (typeof value === 'boolean') {
  2037. const checkboxContainer = document.createElement('div');
  2038. checkboxContainer.style.display = 'flex';
  2039. checkboxContainer.style.alignItems = 'center';
  2040. checkboxContainer.style.justifyContent = 'center';
  2041.  
  2042. const checkbox = document.createElement('input');
  2043. checkbox.type = 'checkbox';
  2044. checkbox.checked = value;
  2045. checkbox.setAttribute('data-key', key);
  2046. checkbox.setAttribute('data-type', 'boolean');
  2047. checkbox.setAttribute('data-type-part', '');
  2048. checkboxContainer.appendChild(checkbox);
  2049.  
  2050. rightContainer.appendChild(checkboxContainer);
  2051. } else if (typeof value === 'number') {
  2052. const numberContainer = document.createElement('div');
  2053. numberContainer.style.display = 'flex';
  2054. numberContainer.style.alignItems = 'center';
  2055. numberContainer.style.justifyContent = 'center';
  2056.  
  2057. const decrementButton = document.createElement('button');
  2058. decrementButton.textContent = '-';
  2059. decrementButton.style.marginRight = '5px';
  2060.  
  2061. const input = document.createElement('span');
  2062. input.textContent = value;
  2063. input.style.margin = '0 10px';
  2064. input.style.minWidth = '3ch';
  2065. input.style.textAlign = 'center';
  2066. input.setAttribute('data-key', key);
  2067. input.setAttribute('data-type', 'number');
  2068. input.setAttribute('data-type-part', '');
  2069.  
  2070. const incrementButton = document.createElement('button');
  2071. incrementButton.textContent = '+';
  2072. incrementButton.style.marginLeft = '5px';
  2073.  
  2074. const updateButtonStates = () => {
  2075. let currentValue = parseFloat(input.textContent);
  2076. if (currentValue <= 0) {
  2077. decrementButton.disabled = true;
  2078. decrementButton.style.color = 'grey';
  2079. } else {
  2080. decrementButton.disabled = false;
  2081. decrementButton.style.color = '';
  2082. }
  2083. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  2084. incrementButton.disabled = true;
  2085. incrementButton.style.color = 'grey';
  2086. } else {
  2087. incrementButton.disabled = false;
  2088. incrementButton.style.color = '';
  2089. }
  2090. };
  2091.  
  2092. decrementButton.addEventListener('click', () => {
  2093. let currentValue = parseFloat(input.textContent);
  2094. if (currentValue > 0) {
  2095. if (currentValue > 200) {
  2096. input.textContent = currentValue - 25;
  2097. } else if (currentValue > 20) {
  2098. input.textContent = currentValue - 5;
  2099. } else {
  2100. input.textContent = currentValue - 1;
  2101. }
  2102. updateButtonStates();
  2103. }
  2104. });
  2105.  
  2106. incrementButton.addEventListener('click', () => {
  2107. let currentValue = parseFloat(input.textContent);
  2108. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  2109. return;
  2110. }
  2111. if (currentValue >= 200) {
  2112. input.textContent = currentValue + 25;
  2113. } else if (currentValue >= 20) {
  2114. input.textContent = currentValue + 5;
  2115. } else {
  2116. input.textContent = currentValue + 1;
  2117. }
  2118. updateButtonStates();
  2119. });
  2120.  
  2121. numberContainer.appendChild(decrementButton);
  2122. numberContainer.appendChild(input);
  2123. numberContainer.appendChild(incrementButton);
  2124.  
  2125. rightContainer.appendChild(numberContainer);
  2126.  
  2127. // Initialize button states
  2128. updateButtonStates();
  2129. } else if (typeof value === 'string') {
  2130. const typeParts = value.split(/(\d+)/).filter(Boolean);
  2131. const numberParts = typeParts.filter(part => !isNaN(part)).map(Number);
  2132.  
  2133. const numberContainer = document.createElement('div');
  2134. numberContainer.style.display = 'flex';
  2135. numberContainer.style.alignItems = 'center';
  2136. numberContainer.style.justifyContent = 'center';
  2137.  
  2138. const typeSpan = document.createElement('span');
  2139. const formattedType = '(' + typeParts.filter(part => isNaN(part)).join('').replace(/_/g, ' ').toLowerCase() + ')';
  2140. typeSpan.textContent = formattedType;
  2141. typeSpan.style.marginRight = '10px';
  2142.  
  2143. leftContainer.appendChild(typeSpan);
  2144.  
  2145. typeParts.forEach(part => {
  2146. if (!isNaN(part)) {
  2147. const decrementButton = document.createElement('button');
  2148. decrementButton.textContent = '-';
  2149. decrementButton.style.marginRight = '5px';
  2150.  
  2151. const input = document.createElement('span');
  2152. input.textContent = part;
  2153. input.style.margin = '0 10px';
  2154. input.style.minWidth = '3ch';
  2155. input.style.textAlign = 'center';
  2156. input.setAttribute('data-key', key);
  2157. input.setAttribute('data-type', 'string');
  2158. input.setAttribute('data-type-part', formattedType);
  2159.  
  2160. const incrementButton = document.createElement('button');
  2161. incrementButton.textContent = '+';
  2162. incrementButton.style.marginLeft = '5px';
  2163.  
  2164. const updateButtonStates = () => {
  2165. let currentValue = parseFloat(input.textContent);
  2166. if (currentValue <= 0) {
  2167. decrementButton.disabled = true;
  2168. decrementButton.style.color = 'grey';
  2169. } else {
  2170. decrementButton.disabled = false;
  2171. decrementButton.style.color = '';
  2172. }
  2173. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  2174. incrementButton.disabled = true;
  2175. incrementButton.style.color = 'grey';
  2176. } else {
  2177. incrementButton.disabled = false;
  2178. incrementButton.style.color = '';
  2179. }
  2180. };
  2181.  
  2182. decrementButton.addEventListener('click', () => {
  2183. let currentValue = parseFloat(input.textContent);
  2184. if (currentValue > 0) {
  2185. if (currentValue > 200) {
  2186. input.textContent = currentValue - 25;
  2187. } else if (currentValue > 20) {
  2188. input.textContent = currentValue - 5;
  2189. } else {
  2190. input.textContent = currentValue - 1;
  2191. }
  2192. updateButtonStates();
  2193. }
  2194. });
  2195.  
  2196. incrementButton.addEventListener('click', () => {
  2197. let currentValue = parseFloat(input.textContent);
  2198. if (key === 'SOUND_VOLUME' && currentValue >= 100) {
  2199. return;
  2200. }
  2201. if (currentValue >= 200) {
  2202. input.textContent = currentValue + 25;
  2203. } else if (currentValue >= 20) {
  2204. input.textContent = currentValue + 5;
  2205. } else {
  2206. input.textContent = currentValue + 1;
  2207. }
  2208. updateButtonStates();
  2209. });
  2210.  
  2211. numberContainer.appendChild(decrementButton);
  2212. numberContainer.appendChild(input);
  2213. numberContainer.appendChild(incrementButton);
  2214.  
  2215. // Initialize button states
  2216. updateButtonStates();
  2217. }
  2218. });
  2219.  
  2220. rightContainer.appendChild(numberContainer);
  2221. } else if (typeof value === 'object') {
  2222. const maxAllowedIndex = hotkeyOptions.length - 1;
  2223.  
  2224. let currentValue = value;
  2225. let choiceIndex = hotkeyOptions.indexOf(currentValue.join(' '));
  2226. if (choiceIndex === -1) {
  2227. currentValue = hotkeyOptions[0].split(' ');
  2228. choiceIndex = 0;
  2229. }
  2230. const textContainer = document.createElement('div');
  2231. textContainer.style.display = 'flex';
  2232. textContainer.style.alignItems = 'center';
  2233. textContainer.style.justifyContent = 'center';
  2234.  
  2235. const decrementButton = document.createElement('button');
  2236. decrementButton.textContent = '<';
  2237. decrementButton.style.marginRight = '5px';
  2238.  
  2239. const input = document.createElement('span');
  2240. input.textContent = currentValue.join(' and ');
  2241. input.style.margin = '0 10px';
  2242. input.style.minWidth = '3ch';
  2243. input.style.textAlign = 'center';
  2244. input.setAttribute('data-key', key);
  2245. input.setAttribute('data-type', 'object');
  2246. input.setAttribute('data-type-part', '');
  2247.  
  2248. const incrementButton = document.createElement('button');
  2249. incrementButton.textContent = '>';
  2250. incrementButton.style.marginLeft = '5px';
  2251.  
  2252. const updateButtonStates = () => {
  2253. if (choiceIndex <= 0) {
  2254. decrementButton.disabled = true;
  2255. decrementButton.style.color = 'grey';
  2256. } else {
  2257. decrementButton.disabled = false;
  2258. decrementButton.style.color = '';
  2259. }
  2260. if (choiceIndex >= maxAllowedIndex) {
  2261. incrementButton.disabled = true;
  2262. incrementButton.style.color = 'grey';
  2263. } else {
  2264. incrementButton.disabled = false;
  2265. incrementButton.style.color = '';
  2266. }
  2267. };
  2268.  
  2269. decrementButton.addEventListener('click', () => {
  2270. if (choiceIndex > 0) {
  2271. choiceIndex -= 1;
  2272. currentValue = hotkeyOptions[choiceIndex].split(' ');
  2273. input.textContent = currentValue.join(' and ');
  2274. updateButtonStates();
  2275. }
  2276. });
  2277.  
  2278. incrementButton.addEventListener('click', () => {
  2279. if (choiceIndex < maxAllowedIndex) {
  2280. choiceIndex += 1;
  2281. currentValue = hotkeyOptions[choiceIndex].split(' ');
  2282. input.textContent = currentValue.join(' and ');
  2283. updateButtonStates();
  2284. }
  2285. });
  2286.  
  2287. textContainer.appendChild(decrementButton);
  2288. textContainer.appendChild(input);
  2289. textContainer.appendChild(incrementButton);
  2290.  
  2291. // Initialize button states
  2292. updateButtonStates();
  2293.  
  2294. rightContainer.appendChild(textContainer);
  2295. }
  2296.  
  2297. optionContainer.appendChild(leftContainer);
  2298. optionContainer.appendChild(rightContainer);
  2299. menuContent.appendChild(optionContainer);
  2300. }
  2301.  
  2302. const menuButtons = createMenuButtons();
  2303. menuContent.appendChild(menuButtons);
  2304.  
  2305. overlay.appendChild(menuContent);
  2306.  
  2307. return overlay;
  2308. }
  2309.  
  2310. function loadConfig() {
  2311. for (const key in localStorage) {
  2312. if (!key.startsWith(scriptPrefix + configPrefix) || !localStorage.hasOwnProperty(key)) {
  2313. continue;
  2314. }
  2315.  
  2316.  
  2317. const configKey = key.substring((scriptPrefix + configPrefix).length); // chop off script prefix and config prefix
  2318. if (!CONFIG.hasOwnProperty(configKey)) {
  2319. continue;
  2320. }
  2321.  
  2322.  
  2323. const savedValue = localStorage.getItem(key);
  2324. if (savedValue === null) {
  2325. continue;
  2326. }
  2327.  
  2328.  
  2329. const valueType = typeof CONFIG[configKey];
  2330. if (configKey === 'RANDOM_SENTENCE') {
  2331. if (savedValue == 0) {
  2332. CONFIG[configKey] = RANDOM_SENTENCE_ENUM.DISABLE;
  2333. }
  2334. if (savedValue == 1) {
  2335. CONFIG[configKey] = RANDOM_SENTENCE_ENUM.ON_FIRST;
  2336. }
  2337. if (savedValue == 2) {
  2338. CONFIG[configKey] = RANDOM_SENTENCE_ENUM.EVERY_TIME;
  2339. }
  2340. } else if (configKey === 'HOTKEYS') {
  2341. CONFIG[configKey] = savedValue.split(' ');
  2342. } else if (valueType === 'boolean') {
  2343. CONFIG[configKey] = savedValue === 'true';
  2344. if (configKey === 'DEFAULT_TO_EXACT_SEARCH') {
  2345. state.exactSearch = CONFIG.DEFAULT_TO_EXACT_SEARCH;
  2346. }
  2347. // I wonder if this is the best way to do this...
  2348. // Probably not because we could just have a single variable to store both, but it would have to be in config and
  2349. // it would be a bit weird to have the program modifying config when the actual config settings aren't changing
  2350. } else if (valueType === 'number') {
  2351. CONFIG[configKey] = parseFloat(savedValue);
  2352. } else if (valueType === 'string') {
  2353. CONFIG[configKey] = savedValue;
  2354. }
  2355. }
  2356. }
  2357.  
  2358.  
  2359. async function process_sentences(state, sentences, first_call) {
  2360. // Early return for empty array or single item (no processing needed)
  2361. if (!sentences || !Array.isArray(sentences) || sentences.length <= 1) {
  2362. return sentences;
  2363. }
  2364. // Only randomize if needed
  2365. const shouldRandomize = CONFIG.RANDOM_SENTENCE >
  2366. (first_call ? RANDOM_SENTENCE_ENUM.DISABLE : RANDOM_SENTENCE_ENUM.ON_FIRST);
  2367.  
  2368. // Skip weight calculation if not needed
  2369. if (!CONFIG.WEIGHTED_SENTENCES && !shouldRandomize) {
  2370. return sentences;
  2371. }
  2372.  
  2373. // Randomize sentences if needed
  2374. if (shouldRandomize) {
  2375. // Use Fisher-Yates shuffle for better performance
  2376. // Only shuffle a maximum of 50 items for large arrays to improve performance
  2377. const maxShuffleItems = Math.min(sentences.length, 50);
  2378.  
  2379. // Optimized weighted random algorithm
  2380. const getWeightedRandomIndex = async (max) => {
  2381. // Pre-calculate weights array for performance
  2382. const weights = new Array(max);
  2383. let totalWeight = 0;
  2384. for (let i = 0; i < max; i++) {
  2385. if (!sentences[i] || !sentences[i].weight) {
  2386. await preprocessSentence(sentences[i]);
  2387. }
  2388. const weight = (sentences[i].weight || 1);
  2389. weights[i] = weight;
  2390. totalWeight += weight;
  2391. }
  2392.  
  2393. // Get random value proportional to total weight
  2394. const random = Math.random() * totalWeight;
  2395. let cumulativeWeight = 0;
  2396.  
  2397. // Find the index
  2398. for (let i = 0; i < max; i++) {
  2399. cumulativeWeight += weights[i];
  2400. if (random <= cumulativeWeight) {
  2401. return i;
  2402. }
  2403. }
  2404.  
  2405. return max - 1; // Fallback
  2406. };
  2407.  
  2408. // Fisher-Yates shuffle with weighted randomization
  2409. for (let i = maxShuffleItems - 1; i > 0; i--) {
  2410. console.log(maxShuffleItems - i, "/", maxShuffleItems)
  2411. const j = CONFIG.WEIGHTED_SENTENCES ?
  2412. await getWeightedRandomIndex(i + 1) :
  2413. Math.floor(Math.random() * (i + 1));
  2414.  
  2415. // Swap elements
  2416. if (i !== j) {
  2417. [sentences[i], sentences[j]] = [sentences[j], sentences[i]];
  2418. }
  2419. }
  2420. }
  2421.  
  2422. return sentences;
  2423. }
  2424.  
  2425. //MAIN FUNCTIONS=====================================================================================================================
  2426. async function onPageLoad() {
  2427. // Initialize state and determine vocabulary based on URL
  2428. state.embedAboveSubsectionMeanings = false;
  2429.  
  2430. // Early layout adjustments without waiting
  2431. setPageWidth();
  2432. const sentenceElement = document.querySelector('.sentence');
  2433. if (sentenceElement) {
  2434. sentenceElement.textContent = "Waiting for data...";
  2435. }
  2436. const machineTranslationFrame = document.getElementById('machine-translation-frame');
  2437. // Skip if machine translation frame is present
  2438. if (machineTranslationFrame) {
  2439. return;
  2440. }
  2441.  
  2442. // Determine the vocabulary based on URL — done in parallel with setting page width
  2443. const url = window.location.href;
  2444. console.log(url)
  2445. if (url.includes('/vocabulary/')) {
  2446. [state.vocab, state.reading] = parseVocabFromVocabulary();
  2447. } else if (url.includes('/search?q=')) { // TODO : get reading from search
  2448. state.vocab = parseVocabFromSearch();
  2449. } else if (url.includes('c=')) {
  2450. [state.vocab, state.reading] = parseVocabFromAnswer();
  2451. } else if (url.includes('/kanji/')) {
  2452. state.vocab = parseVocabFromKanji();
  2453. } else if (url.includes('/deck')) {
  2454. const vocabElements = document.querySelectorAll('.vocabulary-spelling');
  2455. if (vocabElements.length > 0) {
  2456. const preprocessBtn = document.createElement('button');
  2457. preprocessBtn.textContent = 'Preprocess All Words';
  2458. preprocessBtn.style.margin = '10px';
  2459. preprocessBtn.addEventListener('click', async () => {
  2460. for (const vocabElement of vocabElements) {
  2461. const aTag = vocabElement.querySelector('a');
  2462. const href = aTag.getAttribute('href');
  2463.  
  2464. // Split the path into segments
  2465. const segments = href.split('/');
  2466.  
  2467. // The word is in the 4th segment (index 3)
  2468. const lastSegment = segments[3] || '';
  2469.  
  2470. // Remove the fragment part after #
  2471. const vocab = decodeURIComponent(lastSegment.split('#')[0]);
  2472. console.log(vocab)
  2473. if (vocab) {
  2474. await getNadeshikoData(vocab, state.exactSearch);
  2475. }
  2476. }
  2477. alert('Preprocessing complete!');
  2478. });
  2479. const entriesAmountTextElem = [...document.querySelectorAll('p')].find(
  2480. (elem) => elem.innerText.startsWith('Showing') && elem.innerText.endsWith('entries')
  2481. );
  2482. entriesAmountTextElem.parentNode.insertBefore(preprocessBtn, entriesAmountTextElem);
  2483. }
  2484. } else {
  2485. [state.vocab, state.reading] = parseVocabFromReview();
  2486. }
  2487.  
  2488. // Early return if no vocabulary is found
  2489. if (!state.vocab) {
  2490. return;
  2491. }
  2492.  
  2493. // Retrieve stored data for the current vocabulary
  2494. const {sentence, exactState} = getStoredData(state.vocab);
  2495. state.exactSearch = exactState;
  2496.  
  2497. // Fetch data if needed, process in parallel threads where possible
  2498. if (!state.apiDataFetched) {
  2499. try {
  2500. await getNadeshikoData(state.vocab, state.exactSearch);
  2501.  
  2502. // Process sentences in parallel with preloading images
  2503. const processingPromise = process_sentences(state, state.examples, true);
  2504. const preloadPromise = Promise.resolve().then(() => preloadImages());
  2505.  
  2506. state.examples = await processingPromise;
  2507.  
  2508. // Set current example index if a sentence exists
  2509. if (sentence) {
  2510. state.currentExampleIndex = state.examples.findIndex(
  2511. example => example.segment_info.content_jp === sentence
  2512. );
  2513. }
  2514.  
  2515. // Wait for preloading to complete
  2516. await preloadPromise;
  2517.  
  2518. // Finally, display the example
  2519. embedImageAndPlayAudio();
  2520. } catch (error) {
  2521. // Handle errors silently for better performance
  2522. state.error = true;
  2523. embedImageAndPlayAudio(); // Still try to show what we can
  2524. }
  2525. } else if (state.apiDataFetched) {
  2526. // Data already fetched, just update display
  2527. if (sentence) {
  2528. // Process sentence index finding without logging
  2529. state.currentExampleIndex = await process_sentences(
  2530. state,
  2531. state.examples.findIndex(example => example.segment_info.content_jp === sentence),
  2532. false
  2533. );
  2534. }
  2535.  
  2536. // Update display and settings
  2537. Promise.all([
  2538. Promise.resolve().then(() => embedImageAndPlayAudio()),
  2539. Promise.resolve().then(() => setVocabSize())
  2540. ]);
  2541. }
  2542. }
  2543.  
  2544. function setPageWidth() {
  2545. // Set the maximum width of the page
  2546. document.body.style.maxWidth = CONFIG.PAGE_WIDTH;
  2547. }
  2548.  
  2549. // Observe URL changes and reload the page content accordingly
  2550. const observer = new MutationObserver(() => {
  2551. if (window.location.href !== observer.lastUrl) {
  2552. observer.lastUrl = window.location.href;
  2553. onPageLoad();
  2554. }
  2555. });
  2556.  
  2557. // Function to apply styles
  2558. function setVocabSize() {
  2559. // Create a new style element
  2560. const style = document.createElement('style');
  2561. style.type = 'text/css';
  2562. style.innerHTML = `
  2563. .answer-box > .plain {
  2564. font-size: ${CONFIG.VOCAB_SIZE} !important; /* Use the configurable font size */
  2565. padding-bottom: 0.1rem !important; /* Retain padding */
  2566. }
  2567. `;
  2568.  
  2569. // Append the new style to the document head
  2570. document.head.appendChild(style);
  2571. }
  2572.  
  2573. observer.lastUrl = window.location.href;
  2574. observer.observe(document, {subtree: true, childList: true});
  2575.  
  2576. // Add event listeners for page load and URL changes
  2577. window.addEventListener('load', onPageLoad);
  2578. window.addEventListener('popstate', onPageLoad);
  2579. window.addEventListener('hashchange', onPageLoad);
  2580.  
  2581. // Initial configuration and preloading
  2582. loadConfig();
  2583. setPageWidth();
  2584. setVocabSize();
  2585. //preloadImages();
  2586.  
  2587. })();