您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Embeds anime images & audio examples into JPDB review and vocabulary pages using Nadeshiko's API. Compatible only with TamperMonkey.
当前为
- // ==UserScript==
- // @name JPDB Nadeshiko Examples
- // @version 1.22.2
- // @description Embeds anime images & audio examples into JPDB review and vocabulary pages using Nadeshiko's API. Compatible only with TamperMonkey.
- // @author awoo
- // @namespace jpdb-nadeshiko-examples
- // @match https://jpdb.io/review*
- // @match https://jpdb.io/vocabulary/*
- // @match https://jpdb.io/kanji/*
- // @match https://jpdb.io/search*
- // @connect api.brigadasos.xyz
- // @connect linodeobjects.com
- // @connect kanjikana.com
- // @grant GM_addElement
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_registerMenuCommand
- // @license MIT
- // ==/UserScript==
- /*jshint esversion: 6 */
- (function () {
- 'use strict';
- let nadeshikoApiKey = GM_getValue("nadeshiko-api-key", "")
- let jpdbApiKey = GM_getValue("jpdb-api-key", "")
- // Register menu commands
- GM_registerMenuCommand("Set Nadeshiko API Key", async () => {
- nadeshikoApiKey = fetchNadeshikoApiKey();
- });
- GM_registerMenuCommand("Set JPDB API Key", async () => {
- jpdbApiKey = fetchJPDBApiKey();
- })
- function fetchNadeshikoApiKey() {
- 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");
- GM_setValue("nadeshiko-api-key", apiKey);
- if (apiKey) {
- alert("API Key saved successfully!");
- }
- return apiKey;
- }
- function fetchJPDBApiKey() {
- 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.");
- GM_setValue("jpdb-api-key", apiKey);
- if (apiKey) {
- // send ping at https://jpdb.io/api/v1/ping to check if the key is valid
- GM_xmlhttpRequest({
- method: "POST",
- url: "https://jpdb.io/api/v1/ping",
- headers: {
- "Authorization": `Bearer ${apiKey}`,
- },
- onload: function (response) {
- if (response.status === 200) {
- alert("API Key saved successfully!");
- } else {
- alert("Invalid API Key. Please check your key and try again.");
- }
- },
- onerror: function (error) {
- alert("An error occurred while checking the API Key. Please try again.");
- }
- });
- }
- return apiKey;
- }
- // to use custom hotkeys just add them into this array following the same format. Any single keys except space
- // should work. If you want to use special keys, check the linked page for how to represent them in the array
- // (link leads to the arrow keys part so you can compare with the array and be sure which part to write):
- // https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#navigation_keys
- const hotkeyOptions = ['None', 'ArrowLeft ArrowRight', ', .', '[ ]', 'Q W'];
- const RANDOM_SENTENCE_ENUM = {
- DISABLE: 0,
- ON_FIRST: 1,
- EVERY_TIME: 2
- };
- const CONFIG = {
- IMAGE_WIDTH: '400px',
- WIDE_MODE: true,
- DEFINITIONS_ON_RIGHT_IN_WIDE_MODE: false,
- ARROW_WIDTH: '75px',
- ARROW_HEIGHT: '45px',
- PAGE_WIDTH: '75rem',
- SOUND_VOLUME: 80,
- ENABLE_EXAMPLE_TRANSLATION: true,
- SENTENCE_FONT_SIZE: '120%',
- TRANSLATION_FONT_SIZE: '85%',
- COLORED_SENTENCE_TEXT: true,
- AUTO_PLAY_SOUND: true,
- NUMBER_OF_PRELOADS: 1,
- VOCAB_SIZE: '250%',
- MINIMUM_EXAMPLE_LENGTH: 0,
- MAXIMUM_EXAMPLE_LENGTH: 100,
- HOTKEYS: ['None'],
- DEFAULT_TO_EXACT_SEARCH: true,
- // On changing this config option, the icons change but the sentences don't, so you
- // have to click once to match up the icons and again to actually change the sentences
- RANDOM_SENTENCE: RANDOM_SENTENCE_ENUM,
- WEIGHTED_SENTENCES: false,
- };
- const state = {
- currentExampleIndex: 0,
- examples: [],
- apiDataFetched: false,
- vocab: '',
- embedAboveSubsectionMeanings: false,
- preloadedIndices: new Set(),
- currentAudio: null,
- exactSearch: true,
- error: false,
- currentlyPlayingAudio: false,
- reading: '',
- };
- // Prefixing
- const scriptPrefix = 'JPDBNadeshikoExamples-';
- const configPrefix = 'CONFIG.'; // additional prefix for config variables to go after the scriptPrefix
- // do not change either of the above without adding code to handle the change
- const setItem = (key, value) => {
- localStorage.setItem(scriptPrefix + key, value)
- }
- const getItem = (key) => {
- const prefixedValue = localStorage.getItem(scriptPrefix + key);
- if (prefixedValue !== null) {
- return prefixedValue
- }
- const nonPrefixedValue = localStorage.getItem(key);
- // to move away from non-prefixed values as fast as possible
- if (nonPrefixedValue !== null) {
- setItem(key, nonPrefixedValue)
- }
- return nonPrefixedValue
- }
- const removeItem = (key) => {
- localStorage.removeItem(scriptPrefix + key);
- localStorage.removeItem(key)
- }
- // Helper for transitioning to fully script-prefixed config state
- // Deletes all localStorage variables starting with configPrefix and re-adds them with scriptPrefix and configPrefix
- // Danger of other scripts also having localStorage variables starting with configPrefix, so we add a flag showing that
- // we have run this function and make sure it is not set when running it
- // Check for Prefixed flag
- if (localStorage.getItem(`JPDBNadeshiko*Examples-CONFIG_VARIABLES_PREFIXED`) !== 'true') {
- const keysToModify = [];
- // Collect keys that need to be modified
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key.startsWith(configPrefix)) {
- keysToModify.push(key);
- }
- }
- // Modify the collected keys
- keysToModify.forEach((key) => {
- const value = localStorage.getItem(key);
- localStorage.removeItem(key);
- const newKey = scriptPrefix + key;
- localStorage.setItem(newKey, value);
- });
- // Set flag so this only runs once
- // Flag has * in name to place at top in alphabetical sorting,
- // and most importantly, to ensure the flag is never removed or modified
- // by the other script functions that check for the script prefix
- localStorage.setItem(`JPDBNadeshiko*Examples-CONFIG_VARIABLES_PREFIXED`, 'true');
- }
- // IndexedDB Manager
- const IndexedDBManager = {
- MAX_ENTRIES: 100000000,
- EXPIRATION_TIME: 30 * 24 * 60 * 60 * 1000, // 30 days in milliseconds
- open() {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open('NadeshikoDB', 1);
- request.onupgradeneeded = function (event) {
- const db = event.target.result;
- if (!db.objectStoreNames.contains('dataStore')) {
- db.createObjectStore('dataStore', {keyPath: 'keyword'});
- }
- };
- request.onsuccess = function (event) {
- resolve(event.target.result);
- };
- request.onerror = function (event) {
- reject('IndexedDB error: ' + event.target.errorCode);
- };
- });
- },
- get(db, keyword) {
- return new Promise((resolve, reject) => {
- const transaction = db.transaction(['dataStore'], 'readonly');
- const store = transaction.objectStore('dataStore');
- const request = store.get(keyword);
- request.onsuccess = async function (event) {
- const result = event.target.result;
- if (result) {
- const isExpired = Date.now() - result.timestamp >= this.EXPIRATION_TIME;
- const validationError = validateApiResponse(result.data);
- if (isExpired) {
- console.log(`Deleting entry for keyword "${keyword}" because it is expired.`);
- await this.deleteEntry(db, keyword);
- resolve(null);
- } else if (validationError) {
- console.log(`Deleting entry for keyword "${keyword}" due to validation error: ${validationError}`);
- await this.deleteEntry(db, keyword);
- resolve(null);
- } else {
- resolve(result.data);
- }
- } else {
- resolve(null);
- }
- }.bind(this);
- request.onerror = function (event) {
- reject('IndexedDB get error: ' + event.target.errorCode);
- };
- });
- },
- deleteEntry(db, keyword) {
- return new Promise((resolve, reject) => {
- const transaction = db.transaction(['dataStore'], 'readwrite');
- const store = transaction.objectStore('dataStore');
- const request = store.delete(keyword);
- request.onsuccess = () => resolve();
- request.onerror = (e) => reject('IndexedDB delete error: ' + e.target.errorCode);
- });
- },
- getAll(db) {
- return new Promise((resolve, reject) => {
- const transaction = db.transaction(['dataStore'], 'readonly');
- const store = transaction.objectStore('dataStore');
- const entries = [];
- store.openCursor().onsuccess = function (event) {
- const cursor = event.target.result;
- if (cursor) {
- entries.push(cursor.value);
- cursor.continue();
- } else {
- resolve(entries);
- }
- };
- store.openCursor().onerror = function (event) {
- reject('Failed to retrieve entries via cursor: ' + event.target.errorCode);
- };
- });
- },
- save(db, keyword, data) {
- return new Promise(async (resolve, reject) => {
- try {
- const validationError = validateApiResponse(data);
- if (validationError) {
- console.log(`Invalid data detected: ${validationError}. Not saving to IndexedDB.`);
- resolve();
- return;
- }
- // Transform the JSON object to slim it down
- let slimData = {};
- if (data) {
- slimData = data
- } else {
- console.error('Data does not contain expected structure. Cannot slim down.');
- resolve();
- return;
- }
- const entries = await this.getAll(db);
- const transaction = db.transaction(['dataStore'], 'readwrite');
- const store = transaction.objectStore('dataStore');
- if (entries.length >= this.MAX_ENTRIES) {
- // Sort entries by timestamp and delete oldest ones
- entries.sort((a, b) => a.timestamp - b.timestamp);
- const entriesToDelete = entries.slice(0, entries.length - this.MAX_ENTRIES + 1);
- // Delete old entries
- entriesToDelete.forEach(entry => {
- store.delete(entry.keyword).onerror = function () {
- console.error('Failed to delete entry:', entry.keyword);
- };
- });
- }
- // Add the new slimmed entry
- const addRequest = store.put({keyword, data: slimData, timestamp: Date.now()});
- addRequest.onsuccess = () => resolve();
- addRequest.onerror = (e) => reject('IndexedDB save error: ' + e.target.errorCode);
- transaction.oncomplete = function () {
- console.log('IndexedDB updated successfully.');
- };
- transaction.onerror = function (event) {
- reject('IndexedDB update failed: ' + event.target.errorCode);
- };
- } catch (error) {
- reject(`Error in saveToIndexedDB: ${error}`);
- }
- });
- },
- delete() {
- return new Promise((resolve, reject) => {
- const request = indexedDB.deleteDatabase('NadeshikoDB');
- request.onsuccess = function () {
- console.log('IndexedDB deleted successfully');
- resolve();
- };
- request.onerror = function (event) {
- console.error('Error deleting IndexedDB:', event.target.errorCode);
- reject('Error deleting IndexedDB: ' + event.target.errorCode);
- };
- request.onblocked = function () {
- console.warn('Delete operation blocked. Please close all other tabs with this site open and try again.');
- reject('Delete operation blocked');
- };
- });
- }
- };
- // API FUNCTIONS=====================================================================================================================
- function getNadeshikoData(vocab, exactSearch) {
- return new Promise(async (resolve, reject) => {
- const searchVocab = exactSearch ? `"${vocab}"` : vocab;
- const url = `https://api.brigadasos.xyz/api/v1/search/media/sentence`;
- const maxRetries = 5;
- let attempt = 0;
- const storedValue = getItem(state.vocab);
- const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
- // Return early if not blacklisted
- if (isBlacklisted) {
- resolve();
- return;
- }
- async function fetchData() {
- try {
- const db = await IndexedDBManager.open();
- const cachedData = await IndexedDBManager.get(db, searchVocab);
- if (cachedData && Array.isArray(cachedData) && cachedData.length > 0) {
- console.log('Data retrieved from IndexedDB');
- state.examples = cachedData;
- state.apiDataFetched = true;
- resolve();
- } else {
- console.log(`Calling API for: ${searchVocab}`);
- if (!nadeshikoApiKey) {
- // Ask for API Key on search if not set to prevent 401 errors
- nadeshikoApiKey = fetchNadeshikoApiKey();
- if (!nadeshikoApiKey) return;
- }
- GM_xmlhttpRequest({
- method: "POST",
- url: url,
- data: JSON.stringify({query: searchVocab, "limit": 500,}),
- headers:
- {
- "X-API-Key": nadeshikoApiKey,
- "Content-Type": "application/json"
- },
- onload: async function (response) {
- if (response.status === 200) {
- const jsonData = parseJSON(response.response).sentences;
- console.log("API JSON Received");
- const validationError = validateApiResponse(jsonData);
- if (!validationError) {
- state.examples = jsonData;
- state.apiDataFetched = true;
- // check if the sentence is in the vocab
- if (state.vocab && state.reading) {
- const sentenceResults = await Promise.all(
- state.examples.map(async sentence => {
- const foundMatch = await checkVocabInSentence(state, sentence);
- if (!foundMatch) {
- console.log("Removed sentence:", sentence);
- }
- sentence.nulled = true;
- // check if the sentence is too long or too short
- if (sentence.sentence.length < CONFIG.MINIMUM_EXAMPLE_LENGTH || sentence.sentence.length > CONFIG.MAXIMUM_EXAMPLE_LENGTH) {
- console.log("Removed sentence:", sentence);
- return null;
- }
- return sentence;
- })
- );
- // state.examples = sentenceResults.filter(s => s);
- }
- await IndexedDBManager.save(db, searchVocab, jsonData);
- resolve();
- } else {
- attempt++;
- if (attempt < maxRetries) {
- console.log(`Validation error: ${validationError}. Retrying... (${attempt}/${maxRetries})`);
- setTimeout(fetchData, 5000); // Add a 5-second delay before retrying
- } else {
- reject(`Invalid API response after ${maxRetries} attempts: ${validationError}`);
- state.error = true;
- embedImageAndPlayAudio(); //update displayed text
- }
- }
- } else {
- reject(`API call failed with status: ${response.status}`);
- }
- },
- onerror: function (error) {
- reject(`An error occurred: ${error}`);
- }
- });
- }
- } catch (error) {
- reject(`Error: ${error}`);
- }
- }
- fetchData();
- });
- }
- function parseJSON(responseText) {
- try {
- return JSON.parse(responseText);
- } catch (e) {
- console.error('Error parsing JSON:', e);
- return null;
- }
- }
- function validateApiResponse(jsonData) {
- state.error = false;
- if (!jsonData) {
- return 'Not a valid JSON';
- }
- const categoryCount = jsonData.length;
- if (!categoryCount) {
- return 'Missing category count';
- }
- // Check if all category counts are zero
- const allZero = categoryCount == 0
- if (allZero) {
- return 'Blank API';
- }
- return null; // No error
- }
- //FAVORITE DATA FUNCTIONS=====================================================================================================================
- function getStoredData(key) {
- // Retrieve the stored value from localStorage using the provided key
- const storedValue = getItem(key);
- // If a stored value exists, split it into index and exactState
- if (storedValue) {
- const [index, exactState] = storedValue.split(',');
- return {
- index: parseInt(index, 10), // Convert index to an integer
- exactState: exactState === '1' // Convert exactState to a boolean
- };
- }
- // Return default values if no stored value exists
- return {index: 0, exactState: state.exactSearch};
- }
- function storeData(key, sentence, exactState) {
- // Create a string value from index and exactState to store in localStorage
- const value = `${sentence},${exactState ? 1 : 0}`;
- // Store the value in localStorage using the provided key
- setItem(key, value);
- }
- // PARSE VOCAB FUNCTIONS =====================================================================================================================
- function parseVocabFromAnswer() {
- // Select all links containing "/kanji/" or "/vocabulary/" in the href attribute
- const elements = document.querySelectorAll('a[href*="/kanji/"], a[href*="/vocabulary/"]');
- console.log("Parsing Answer Page");
- // Iterate through the matched elements
- for (const element of elements) {
- const href = element.getAttribute('href');
- const text = element.textContent.trim();
- // Match the href to extract kanji or vocabulary (ignoring ID if present)
- const match = href.match(/\/(kanji|vocabulary)\/(?:\d+\/)?([^\#]*)#/);
- if (match) return match[2].trim();
- if (text) return text.trim();
- }
- return '';
- }
- function parseVocabFromReview() {
- console.log("Parsing Review Page");
- // Select the element with class 'kind' to determine the type of content
- const kindElement = document.querySelector('.kind');
- // If kindElement doesn't exist, set kindText to null
- const kindText = kindElement ? kindElement.textContent.trim() : null;
- // Accept 'Kanji' or 'Vocabulary' kindText
- if (kindText !== 'Kanji' && kindText !== 'Vocabulary') {
- console.log("Not Kanji or existing Vocabulary. Attempting to parse New Vocab.");
- // Attempt to parse from <a> tag with specific pattern
- const anchorElement = document.querySelector('a.plain[href*="/vocabulary/"]');
- if (anchorElement) {
- const href = anchorElement.getAttribute('href');
- const match = href.match(/\/vocabulary\/\d+\/([^#]+)#a/);
- if (match && match[1]) {
- const new_vocab = match[1];
- console.log("Found New Vocab:", new_vocab);
- return new_vocab;
- }
- }
- console.log("No Vocabulary found.");
- return '';
- }
- if (kindText === 'Vocabulary') {
- // Select the element with class 'plain' to extract vocabulary
- const plainElement = document.querySelector('.plain');
- if (!plainElement) {
- return '';
- }
- const rubyElements = plainElement.querySelectorAll('ruby');
- // Extract the text from <rt> children and join them.
- let vocabulary = ""
- const reading = Array.from(rubyElements)
- .map(ruby => {
- const rtElement = ruby.querySelector('rt');
- // add the text not in the <rt> tag to the vocabulary
- vocabulary = vocabulary + (ruby.childNodes[0] ? ruby.childNodes[0].textContent.trim() : '');
- if (rtElement) {
- rtElement.style.display = 'none';
- return rtElement.textContent.trim();
- }
- return '';
- })
- .join('');
- // Regular expression to check if the vocabulary contains kanji characters
- const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
- if (kanjiRegex.test(vocabulary) || vocabulary) {
- console.log("Found Vocabulary:", vocabulary);
- return [vocabulary, reading];
- }
- } else if (kindText === 'Kanji') {
- // Select the hidden input element to extract kanji
- const hiddenInput = document.querySelector('input[name="c"]');
- if (!hiddenInput) {
- return '';
- }
- const vocab = hiddenInput.value.split(',')[1];
- const kanjiRegex = /[\u4e00-\u9faf\u3400-\u4dbf]/;
- if (kanjiRegex.test(vocab)) {
- console.log("Found Kanji:", vocab);
- return vocab;
- }
- }
- console.log("No Vocabulary or Kanji found.");
- return '';
- }
- function parseVocabFromVocabulary() {
- // Get the current URL
- let url = window.location.href;
- // Remove query parameters (e.g., ?lang=english) and fragment identifiers (#)
- url = url.split('?')[0].split('#')[0];
- // Match the URL structure for a vocabulary page
- const match = url.match(/https:\/\/jpdb\.io\/vocabulary\/(\d+)\/([^\#\/]*)\/([^\#\/]*)/);
- console.log("Parsing Vocabulary Page");
- if (match) {
- // Extract and decode the vocabulary part from the URL
- let vocab = match[2];
- state.embedAboveSubsectionMeanings = true; // Set state flag
- let reading = match[3];
- return [decodeURIComponent(vocab), decodeURIComponent(reading)];
- }
- // Return empty string if no match
- return '';
- }
- function parseVocabFromKanji() {
- // Get the current URL
- const url = window.location.href;
- // Match the URL structure for a kanji page
- const match = url.match(/https:\/\/jpdb\.io\/kanji\/(\d+)\/([^\#]*)#a/);
- console.log("Parsing Kanji Page");
- if (match) {
- // Extract and decode the kanji part from the URL
- let kanji = match[2];
- state.embedAboveSubsectionMeanings = true; // Set state flag
- kanji = kanji.split('/')[0];
- return decodeURIComponent(kanji);
- }
- // Return empty string if no match
- return '';
- }
- function parseVocabFromSearch() {
- // Get the current URL
- let url = window.location.href;
- // Match the URL structure for a search query, capturing the vocab between `?q=` and either `&` or `+`
- const match = url.match(/https:\/\/jpdb\.io\/search\?q=([^&+]*)/);
- console.log("Parsing Search Page");
- if (match) {
- // Extract and decode the vocabulary part from the URL
- let vocab = match[1];
- return decodeURIComponent(vocab);
- }
- // Return empty string if no match
- return '';
- }
- //EMBED FUNCTIONS=====================================================================================================================
- function createAnchor(marginLeft) {
- // Create and style an anchor element
- const anchor = document.createElement('a');
- anchor.href = '#';
- anchor.style.border = '0';
- anchor.style.display = 'inline-flex';
- anchor.style.verticalAlign = 'middle';
- anchor.style.marginLeft = marginLeft;
- return anchor;
- }
- function createIcon(iconClass, fontSize = '1.4rem', color = '#3d81ff') {
- // Create and style an icon element
- const icon = document.createElement('i');
- icon.className = iconClass;
- icon.style.fontSize = fontSize;
- icon.style.opacity = '1.0';
- icon.style.verticalAlign = 'baseline';
- icon.style.color = color;
- return icon;
- }
- function createSpeakerButton(soundUrl) {
- // Create a speaker button with an icon and click event for audio playback
- const anchor = createAnchor('0.5rem');
- const icon = createIcon('ti ti-volume');
- anchor.appendChild(icon);
- anchor.addEventListener('click', (event) => {
- event.preventDefault();
- playAudio(soundUrl);
- });
- return anchor;
- }
- function createStarButton() {
- // Create a star button with an icon and click event for toggling favorite state
- const anchor = createAnchor('0.5rem');
- const starIcon = document.createElement('span');
- const storedValue = getItem(state.vocab);
- // console.log(storedValue);
- // Determine the star icon (filled or empty) based on stored value
- if (storedValue) {
- const [storedIndex, storedExactState] = storedValue.split(',');
- const index = parseInt(storedIndex, 10);
- const exactState = Boolean(parseInt(storedExactState, 10));
- starIcon.textContent = (state.currentExampleIndex === index && state.exactSearch === exactState) ? '★' : '☆';
- } else {
- starIcon.textContent = '☆';
- }
- // Style the star icon
- starIcon.style.fontSize = '1.4rem';
- starIcon.style.color = '#3D8DFF';
- starIcon.style.verticalAlign = 'middle';
- starIcon.style.position = 'relative';
- starIcon.style.top = '-2px';
- // Append the star icon to the anchor and set up the click event to toggle star state
- anchor.appendChild(starIcon);
- anchor.addEventListener('click', (event) => {
- event.preventDefault();
- toggleStarState(starIcon);
- });
- return anchor;
- }
- function toggleStarState(starIcon) {
- const storedValue = getItem(state.vocab);
- const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
- // Return early if blacklisted
- if (isBlacklisted) {
- starIcon.textContent = '☆';
- return;
- }
- // Toggle the star state between filled and empty
- if (storedValue) {
- const [storedIndex, storedExactState] = storedValue.split(',');
- const index = parseInt(storedIndex, 10);
- const exactState = storedExactState === '1';
- if (index === state.currentExampleIndex && exactState === state.exactSearch) {
- removeItem(state.vocab);
- starIcon.textContent = '☆';
- } else {
- setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`);
- starIcon.textContent = '★';
- }
- } else {
- setItem(state.vocab, `${state.currentExampleIndex},${state.exactSearch ? 1 : 0}`);
- starIcon.textContent = '★';
- }
- }
- function createQuoteButton() {
- // Create a quote button with an icon and click event for toggling quote style
- const anchor = createAnchor('0rem');
- const quoteIcon = document.createElement('span');
- // Set the icon based on exact search state
- quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』';
- // Style the quote icon
- quoteIcon.style.fontSize = '1.1rem';
- quoteIcon.style.color = '#3D8DFF';
- quoteIcon.style.verticalAlign = 'middle';
- quoteIcon.style.position = 'relative';
- quoteIcon.style.top = '0px';
- // Append the quote icon to the anchor and set up the click event to toggle quote state
- anchor.appendChild(quoteIcon);
- anchor.addEventListener('click', (event) => {
- event.preventDefault();
- toggleQuoteState(quoteIcon);
- });
- return anchor;
- }
- function toggleQuoteState(quoteIcon) {
- const storedValue = getItem(state.vocab);
- const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
- // Return early if blacklisted
- if (isBlacklisted) {
- return;
- }
- // Toggle between single and double quote styles
- state.exactSearch = !state.exactSearch;
- quoteIcon.innerHTML = state.exactSearch ? '<b>「」</b>' : '『』';
- // Update state based on stored data
- const storedData = getStoredData(state.vocab);
- if (storedData && storedData.exactState === state.exactSearch) {
- state.currentExampleIndex = storedData.index;
- } else {
- state.currentExampleIndex = 0;
- }
- state.apiDataFetched = false;
- embedImageAndPlayAudio();
- getNadeshikoData(state.vocab, state.exactSearch)
- .then(() => {
- embedImageAndPlayAudio();
- })
- .catch(error => {
- console.error(error);
- });
- }
- function createMenuButton() {
- // Create a menu button with a dropdown menu
- const anchor = createAnchor('0.5rem');
- const menuIcon = document.createElement('span');
- menuIcon.innerHTML = '☰';
- // Style the menu icon
- menuIcon.style.fontSize = '1.4rem';
- menuIcon.style.color = '#3D8DFF';
- menuIcon.style.verticalAlign = 'middle';
- menuIcon.style.position = 'relative';
- menuIcon.style.top = '-2px';
- // Append the menu icon to the anchor and set up the click event to show the overlay menu
- anchor.appendChild(menuIcon);
- anchor.addEventListener('click', (event) => {
- event.preventDefault();
- const overlay = createOverlayMenu();
- document.body.appendChild(overlay);
- });
- return anchor;
- }
- function createTextButton(vocab, exact) {
- // Create a text button for Nadeshiko
- const textButton = document.createElement('a');
- textButton.textContent = 'Nadeshiko';
- textButton.style.color = 'var(--subsection-label-color)';
- textButton.style.fontSize = '85%';
- textButton.style.marginRight = '0.5rem';
- textButton.style.verticalAlign = 'middle';
- textButton.href = `https://nadeshiko.co/search/sentence?query=${encodeURIComponent(vocab)}`;
- textButton.target = '_blank';
- return textButton;
- }
- function createButtonContainer(soundUrl, vocab, exact) {
- // Create a container for all buttons
- const buttonContainer = document.createElement('div');
- buttonContainer.className = 'button-container';
- buttonContainer.style.display = 'flex';
- buttonContainer.style.justifyContent = 'space-between';
- buttonContainer.style.alignItems = 'center';
- buttonContainer.style.marginBottom = '5px';
- buttonContainer.style.lineHeight = '1.4rem';
- // Create individual buttons
- const menuButton = createMenuButton();
- const textButton = createTextButton(vocab, exact);
- const speakerButton = createSpeakerButton(soundUrl);
- const starButton = createStarButton();
- const quoteButton = createQuoteButton();
- // Center the buttons within the container
- const centeredButtonsWrapper = document.createElement('div');
- centeredButtonsWrapper.style.display = 'flex';
- centeredButtonsWrapper.style.justifyContent = 'center';
- centeredButtonsWrapper.style.flex = '1';
- centeredButtonsWrapper.append(textButton, speakerButton, starButton, quoteButton);
- buttonContainer.append(centeredButtonsWrapper, menuButton);
- return buttonContainer;
- }
- function stopCurrentAudio() {
- // Stop any currently playing audio
- if (state.currentAudio) {
- state.currentAudio.source.stop();
- state.currentAudio.context.close();
- state.currentAudio = null;
- }
- }
- function playAudio(soundUrl) {
- // Skip playing audio if it is already playing
- if (state.currentlyPlayingAudio) {
- //console.log('Duplicate audio was skipped.');
- return;
- }
- if (soundUrl) {
- state.currentlyPlayingAudio = true;
- stopCurrentAudio();
- GM_xmlhttpRequest({
- method: 'GET',
- url: soundUrl,
- responseType: 'arraybuffer',
- onload: function (response) {
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- audioContext.decodeAudioData(response.response, function (buffer) {
- const source = audioContext.createBufferSource();
- source.buffer = buffer;
- const gainNode = audioContext.createGain();
- // Connect the source to the gain node and the gain node to the destination
- source.connect(gainNode);
- gainNode.connect(audioContext.destination);
- // Mute the first part and then ramp up the volume
- gainNode.gain.setValueAtTime(0, audioContext.currentTime);
- gainNode.gain.linearRampToValueAtTime(CONFIG.SOUND_VOLUME / 100, audioContext.currentTime + 0.1);
- // Play the audio, skip the first part to avoid any "pop"
- source.start(0, 0.05);
- // Log when the audio starts playing
- //console.log('Audio has started playing.');
- // Save the current audio context and source for stopping later
- state.currentAudio = {
- context: audioContext,
- source: source
- };
- // Set currentlyPlayingAudio to false when the audio ends
- source.onended = function () {
- state.currentlyPlayingAudio = false;
- };
- }, function (error) {
- console.error('Error decoding audio:', error);
- state.currentlyPlayingAudio = false;
- });
- },
- onerror: function (error) {
- console.error('Error fetching audio:', error);
- state.currentlyPlayingAudio = false;
- }
- });
- }
- }
- // has to be declared (referenced in multiple functions but definition requires variables local to one function)
- let hotkeysListener;
- function renderImageAndPlayAudio(vocab, shouldAutoPlaySound) {
- if (state.apiDataFetched === false) {
- console.log("No data");
- return;
- }
- const example = state.examples[state.currentExampleIndex] || {};
- const imageUrl = example.media_info?.path_image || null;
- const soundUrl = example.media_info?.path_audio || null;
- const sentence = example.segment_info?.content_jp || null;
- const translation = example.segment_info?.content_en || "";
- const deck_name = example.basic_info?.name_anime_romaji || "Unknown Anime";
- const storedValue = getItem(state.vocab);
- const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
- // Update sentence class content with actual sentence text
- const sentenceElement = document.querySelector('.sentence');
- if (sentenceElement) {
- sentenceElement.textContent = sentence;
- }
- // Update translation class content with actual translation text
- const translationElement = document.querySelector('.sentence-translation');
- if (translationElement) {
- translationElement.textContent = translation;
- }
- // Remove any existing container
- removeExistingContainer();
- if (!shouldRenderContainer()) return;
- // Create and append the main wrapper and text button container
- const wrapperDiv = createWrapperDiv();
- const textDiv = createButtonContainer(soundUrl, vocab, state.exactSearch);
- wrapperDiv.appendChild(textDiv);
- const createTextElement = (text) => {
- const textElement = document.createElement('div');
- textElement.textContent = text;
- textElement.style.padding = '100px 0';
- textElement.style.whiteSpace = 'pre'; // Ensures newlines are respected
- return textElement;
- };
- if (isBlacklisted) {
- wrapperDiv.appendChild(createTextElement('BLACKLISTED'));
- shouldAutoPlaySound = false;
- } else if (state.apiDataFetched) {
- if (imageUrl) {
- const imageElement = createImageElement(wrapperDiv, imageUrl, vocab, state.exactSearch);
- if (imageElement) {
- imageElement.addEventListener('click', () => playAudio(soundUrl));
- }
- } else {
- wrapperDiv.appendChild(createTextElement(`NO IMAGE\n(${deck_name})`));
- }
- // Append sentence and translation or a placeholder text
- // sentence ? appendSentenceAndTranslation(wrapperDiv, sentence, translation) : appendNoneText(wrapperDiv);
- } else if (!sentence) {
- wrapperDiv.appendChild(createTextElement('ERROR\nNO EXAMPLES FOUND\n\nRARE WORD OR NADESHIKO API IS TEMPORARILY DOWN'));
- } else {
- wrapperDiv.appendChild(createTextElement('LOADING'));
- }
- // Create navigation elements
- const navigationDiv = createNavigationDiv();
- const leftArrow = createLeftArrow(vocab, shouldAutoPlaySound);
- const rightArrow = createRightArrow(vocab, shouldAutoPlaySound);
- // Create and append the main container
- const containerDiv = createContainerDiv(leftArrow, wrapperDiv, rightArrow, navigationDiv);
- appendContainer(containerDiv);
- // Auto-play sound if configured
- if (CONFIG.AUTO_PLAY_SOUND && shouldAutoPlaySound) {
- playAudio(soundUrl);
- }
- // Link hotkeys
- if (CONFIG.HOTKEYS.indexOf("None") === -1) {
- const leftHotkey = CONFIG.HOTKEYS[0];
- const rightHotkey = CONFIG.HOTKEYS[1];
- hotkeysListener = (event) => {
- if (event.repeat) return;
- switch (event.key.toLowerCase()) {
- case leftHotkey.toLowerCase():
- if (leftArrow.disabled) {
- // listener gets removed, so need to re-add
- window.addEventListener('keydown', hotkeysListener, {once: true});
- } else {
- leftArrow.click(); // don't need to re-add listener because renderImageAndPlayAudio() will run again
- }
- break;
- case rightHotkey.toLowerCase():
- if (rightArrow.disabled) {
- // listener gets removed, so need to re-add
- window.addEventListener('keydown', hotkeysListener, {once: true});
- } else {
- rightArrow.click(); // don't need to re-add listener because renderImageAndPlayAudio() will run again
- }
- break;
- default:
- // listener gets removed, so need to re-add
- window.addEventListener('keydown', hotkeysListener, {once: true});
- }
- }
- window.addEventListener('keydown', hotkeysListener, {once: true});
- }
- }
- function removeExistingContainer() {
- // Remove the existing container if it exists
- const existingContainer = document.getElementById('nadeshiko-container');
- if (existingContainer) {
- existingContainer.remove();
- }
- window.removeEventListener('keydown', hotkeysListener);
- }
- function shouldRenderContainer() {
- // Determine if the container should be rendered based on the presence of certain elements
- const resultVocabularySection = document.querySelector('.result.vocabulary');
- const hboxWrapSection = document.querySelector('.hbox.wrap');
- const subsectionMeanings = document.querySelector('.subsection-meanings');
- const subsectionLabels = document.querySelectorAll('h6.subsection-label');
- return resultVocabularySection || hboxWrapSection || subsectionMeanings || subsectionLabels.length >= 3;
- }
- function createWrapperDiv() {
- // Create and style the wrapper div
- const wrapperDiv = document.createElement('div');
- wrapperDiv.id = 'image-wrapper';
- wrapperDiv.style.textAlign = 'center';
- wrapperDiv.style.padding = '5px 0';
- return wrapperDiv;
- }
- function createImageElement(wrapperDiv, imageUrl, vocab, exactSearch) {
- // Create and return an image element with specified attributes
- const searchVocab = exactSearch ? `「${vocab}」` : vocab;
- const example = state.examples[state.currentExampleIndex] || {};
- const deck_name = example.basic_info.name_anime_romaji || null;
- // Extract the file name from the URL
- let file_name = imageUrl.substring(imageUrl.lastIndexOf('/') + 1);
- // Remove prefixes "Anime_", "A_", or "Z" from the file name
- file_name = file_name.replace(/^(Anime_|A_|Z)/, '');
- const titleText = `${searchVocab} #${state.currentExampleIndex + 1} \n${deck_name} \n${file_name}`;
- return GM_addElement(wrapperDiv, 'img', {
- src: imageUrl,
- alt: 'Embedded Image',
- title: titleText,
- style: `max-width: ${CONFIG.IMAGE_WIDTH}; margin-top: 10px; cursor: pointer;`
- });
- }
- function highlightVocab(sentence, vocab) {
- // Highlight vocabulary in the sentence based on configuration
- if (!CONFIG.COLORED_SENTENCE_TEXT) return sentence;
- if (state.exactSearch) {
- const regex = new RegExp(`(${vocab})`, 'g');
- return sentence.replace(regex, '<span style="color: var(--outline-input-color);">$1</span>');
- } else {
- return vocab.split('').reduce((acc, char) => {
- const regex = new RegExp(char, 'g');
- return acc.replace(regex, `<span style="color: var(--outline-input-color);">${char}</span>`);
- }, sentence);
- }
- }
- function appendSentenceAndTranslation(wrapperDiv, sentence, translation) {
- // Append sentence and translation to the wrapper div
- const sentenceText = document.createElement('div');
- sentenceText.innerHTML = highlightVocab(sentence, state.vocab);
- sentenceText.style.marginTop = '10px';
- sentenceText.style.fontSize = CONFIG.SENTENCE_FONT_SIZE;
- sentenceText.style.color = 'lightgray';
- sentenceText.style.maxWidth = CONFIG.IMAGE_WIDTH;
- sentenceText.style.whiteSpace = 'pre-wrap';
- wrapperDiv.appendChild(sentenceText);
- if (CONFIG.ENABLE_EXAMPLE_TRANSLATION && translation) {
- const translationText = document.createElement('div');
- translationText.innerHTML = replaceSpecialCharacters(translation);
- translationText.style.marginTop = '5px';
- translationText.style.fontSize = CONFIG.TRANSLATION_FONT_SIZE;
- translationText.style.color = 'var(--subsection-label-color)';
- translationText.style.maxWidth = CONFIG.IMAGE_WIDTH;
- translationText.style.whiteSpace = 'pre-wrap';
- wrapperDiv.appendChild(translationText);
- }
- }
- function appendNoneText(wrapperDiv) {
- // Append a "None" text to the wrapper div
- const noneText = document.createElement('div');
- noneText.textContent = 'None';
- noneText.style.marginTop = '10px';
- noneText.style.fontSize = '85%';
- noneText.style.color = 'var(--subsection-label-color)';
- wrapperDiv.appendChild(noneText);
- }
- function createNavigationDiv() {
- // Create and style the navigation div
- const navigationDiv = document.createElement('div');
- navigationDiv.id = 'nadeshiko-embed';
- navigationDiv.style.display = 'flex';
- navigationDiv.style.justifyContent = 'center';
- navigationDiv.style.alignItems = 'center';
- navigationDiv.style.maxWidth = CONFIG.IMAGE_WIDTH;
- navigationDiv.style.margin = '0 auto';
- return navigationDiv;
- }
- function createLeftArrow(vocab, shouldAutoPlaySound) {
- // Create and configure the left arrow button
- const leftArrow = document.createElement('button');
- leftArrow.textContent = '<';
- leftArrow.style.marginRight = '10px';
- leftArrow.style.width = CONFIG.ARROW_WIDTH;
- leftArrow.style.height = CONFIG.ARROW_HEIGHT;
- leftArrow.style.lineHeight = '25px';
- leftArrow.style.textAlign = 'center';
- leftArrow.style.display = 'flex';
- leftArrow.style.justifyContent = 'center';
- leftArrow.style.alignItems = 'center';
- leftArrow.style.padding = '0'; // Remove padding
- leftArrow.disabled = state.currentExampleIndex === 0;
- leftArrow.addEventListener('click', () => {
- if (state.currentExampleIndex > 0) {
- state.currentExampleIndex--;
- state.currentlyPlayingAudio = false;
- renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
- preloadImages();
- }
- });
- return leftArrow;
- }
- function createRightArrow(vocab, shouldAutoPlaySound) {
- // Create and configure the right arrow button
- const rightArrow = document.createElement('button');
- rightArrow.textContent = '>';
- rightArrow.style.marginLeft = '10px';
- rightArrow.style.width = CONFIG.ARROW_WIDTH;
- rightArrow.style.height = CONFIG.ARROW_HEIGHT;
- rightArrow.style.lineHeight = '25px';
- rightArrow.style.textAlign = 'center';
- rightArrow.style.display = 'flex';
- rightArrow.style.justifyContent = 'center';
- rightArrow.style.alignItems = 'center';
- rightArrow.style.padding = '0'; // Remove padding
- rightArrow.disabled = state.currentExampleIndex >= state.examples.length - 1;
- rightArrow.addEventListener('click', () => {
- if (state.currentExampleIndex < state.examples.length - 1) {
- state.currentExampleIndex++;
- state.currentlyPlayingAudio = false;
- renderImageAndPlayAudio(vocab, shouldAutoPlaySound);
- preloadImages();
- }
- });
- return rightArrow;
- }
- function createContainerDiv(leftArrow, wrapperDiv, rightArrow, navigationDiv) {
- // Create and configure the main container div
- const containerDiv = document.createElement('div');
- containerDiv.id = 'nadeshiko-container';
- containerDiv.style.display = 'flex';
- containerDiv.style.alignItems = 'center';
- containerDiv.style.justifyContent = 'center';
- containerDiv.style.flexDirection = 'column';
- const arrowWrapperDiv = document.createElement('div');
- arrowWrapperDiv.style.display = 'flex';
- arrowWrapperDiv.style.alignItems = 'center';
- arrowWrapperDiv.style.justifyContent = 'center';
- arrowWrapperDiv.append(leftArrow, wrapperDiv, rightArrow);
- containerDiv.append(arrowWrapperDiv, navigationDiv);
- return containerDiv;
- }
- function appendContainer(containerDiv) {
- // Append the container div to the appropriate section based on configuration
- const resultVocabularySection = document.querySelector('.result.vocabulary');
- const hboxWrapSection = document.querySelector('.hbox.wrap');
- const subsectionMeanings = document.querySelector('.subsection-meanings');
- const subsectionComposedOfKanji = document.querySelector('.subsection-composed-of-kanji');
- const subsectionPitchAccent = document.querySelector('.subsection-pitch-accent');
- const subsectionLabels = document.querySelectorAll('h6.subsection-label');
- const vboxGap = document.querySelector('.vbox.gap');
- const styleSheet = document.querySelector('link[rel="stylesheet"]').sheet;
- if (CONFIG.WIDE_MODE && subsectionMeanings) {
- const wrapper = document.createElement('div');
- wrapper.style.display = 'flex';
- wrapper.style.alignItems = 'flex-start';
- styleSheet.insertRule('.subsection-meanings { max-width: none !important; }', styleSheet.cssRules.length);
- const originalContentWrapper = document.createElement('div');
- originalContentWrapper.style.flex = '1';
- originalContentWrapper.appendChild(subsectionMeanings);
- if (subsectionComposedOfKanji) {
- const newline1 = document.createElement('br');
- originalContentWrapper.appendChild(newline1);
- originalContentWrapper.appendChild(subsectionComposedOfKanji);
- }
- if (subsectionPitchAccent) {
- const newline2 = document.createElement('br');
- originalContentWrapper.appendChild(newline2);
- originalContentWrapper.appendChild(subsectionPitchAccent);
- }
- if (CONFIG.DEFINITIONS_ON_RIGHT_IN_WIDE_MODE) {
- wrapper.appendChild(containerDiv);
- wrapper.appendChild(originalContentWrapper);
- } else {
- wrapper.appendChild(originalContentWrapper);
- wrapper.appendChild(containerDiv);
- }
- if (vboxGap) {
- const existingDynamicDiv = vboxGap.querySelector('#dynamic-content');
- if (existingDynamicDiv) {
- existingDynamicDiv.remove();
- }
- const dynamicDiv = document.createElement('div');
- dynamicDiv.id = 'dynamic-content';
- dynamicDiv.appendChild(wrapper);
- if (window.location.href.includes('vocabulary')) {
- vboxGap.insertBefore(dynamicDiv, vboxGap.children[1]);
- } else {
- vboxGap.insertBefore(dynamicDiv, vboxGap.firstChild);
- }
- }
- } else {
- if (state.embedAboveSubsectionMeanings && subsectionMeanings) {
- subsectionMeanings.parentNode.insertBefore(containerDiv, subsectionMeanings);
- } else if (resultVocabularySection) {
- resultVocabularySection.parentNode.insertBefore(containerDiv, resultVocabularySection);
- } else if (hboxWrapSection) {
- hboxWrapSection.parentNode.insertBefore(containerDiv, hboxWrapSection);
- } else if (subsectionLabels.length >= 4) {
- subsectionLabels[3].parentNode.insertBefore(containerDiv, subsectionLabels[3]);
- }
- }
- }
- function embedImageAndPlayAudio() {
- // Embed the image and play audio, removing existing navigation div if present
- console.log("Embedding image and playing audio");
- const existingNavigationDiv = document.getElementById('nadeshiko-embed');
- if (existingNavigationDiv) existingNavigationDiv.remove();
- const reviewUrlPattern = /https:\/\/jpdb\.io\/review(#a)?$/;
- renderImageAndPlayAudio(state.vocab, !reviewUrlPattern.test(window.location.href));
- preloadImages();
- }
- function replaceSpecialCharacters(text) {
- // Replace special characters in the text
- return text.replace(/<br>/g, '\n').replace(/"/g, '"').replace(/\n/g, '<br>');
- }
- function preloadImages() {
- // Preload images around the current example index
- const preloadDiv = GM_addElement(document.body, 'div', {style: 'display: none;'});
- const startIndex = Math.max(0, state.currentExampleIndex - CONFIG.NUMBER_OF_PRELOADS);
- const endIndex = Math.min(state.examples.length - 1, state.currentExampleIndex + CONFIG.NUMBER_OF_PRELOADS);
- for (let i = startIndex; i <= endIndex; i++) {
- if (!state.preloadedIndices.has(i) && state.examples[i].image_url) {
- GM_addElement(preloadDiv, 'img', {src: state.examples[i].image_url});
- state.preloadedIndices.add(i);
- }
- }
- }
- //MENU FUNCTIONS=====================================================================================================================
- ////FILE OPERATIONS=====================================================================================================================
- function handleImportButtonClick() {
- handleFileInput('application/json', importFavorites);
- }
- function handleImportDButtonClick() {
- handleFileInput('application/json', importData);
- }
- function handleFileInput(acceptType, callback) {
- const fileInput = document.createElement('input');
- fileInput.type = 'file';
- fileInput.accept = acceptType;
- fileInput.addEventListener('change', callback);
- fileInput.click();
- }
- function createBlobAndDownload(data, filename, type) {
- const blob = new Blob([data], {type});
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
- function addBlacklist() {
- setItem(state.vocab, `0,2`);
- location.reload();
- }
- function remBlacklist() {
- removeItem(state.vocab);
- location.reload();
- }
- function exportFavorites() {
- const favorites = {};
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key.startsWith(scriptPrefix)) {
- const keyPrefixless = key.substring(scriptPrefix.length); // chop off the script prefix
- if (!keyPrefixless.startsWith(configPrefix)) {
- favorites[keyPrefixless] = localStorage.getItem(key);
- // For backwards compatibility keep the exported keys prefixless
- }
- }
- }
- const data = JSON.stringify(favorites, null, 2);
- createBlobAndDownload(data, 'favorites.json', 'application/json');
- }
- function importFavorites(event) {
- const file = event.target.files[0];
- if (!file) return;
- const reader = new FileReader();
- reader.onload = function (e) {
- try {
- const favorites = JSON.parse(e.target.result);
- for (const key in favorites) {
- setItem(key, favorites[key]);
- }
- alert('Favorites imported successfully!');
- location.reload();
- } catch (error) {
- alert('Error importing favorites:', error);
- }
- };
- reader.readAsText(file);
- }
- async function exportData() {
- const dataEntries = {};
- try {
- const db = await IndexedDBManager.open();
- const indexedDBData = await IndexedDBManager.getAll(db);
- indexedDBData.forEach(item => {
- dataEntries[item.keyword] = item.data;
- });
- const data = JSON.stringify(dataEntries, null, 2);
- createBlobAndDownload(data, 'data.json', 'application/json');
- } catch (error) {
- console.error('Error exporting data from IndexedDB:', error);
- }
- }
- async function importData(event) {
- const file = event.target.files[0];
- if (!file) return;
- const reader = new FileReader();
- reader.onload = async function (e) {
- try {
- const dataEntries = JSON.parse(e.target.result);
- const db = await IndexedDBManager.open();
- for (const key in dataEntries) {
- await IndexedDBManager.save(db, key, dataEntries[key]);
- }
- alert('Data imported successfully!');
- location.reload();
- } catch (error) {
- alert('Error importing data:', error);
- }
- };
- reader.readAsText(file);
- }
- ////CONFIRMATION
- function createConfirmationPopup(messageText, onYes, onNo) {
- // Create a confirmation popup with Yes and No buttons
- const popupOverlay = document.createElement('div');
- popupOverlay.style.position = 'fixed';
- popupOverlay.style.top = '0';
- popupOverlay.style.left = '0';
- popupOverlay.style.width = '100%';
- popupOverlay.style.height = '100%';
- popupOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
- popupOverlay.style.zIndex = '1001';
- popupOverlay.style.display = 'flex';
- popupOverlay.style.justifyContent = 'center';
- popupOverlay.style.alignItems = 'center';
- const popupContent = document.createElement('div');
- popupContent.style.backgroundColor = 'var(--background-color)';
- popupContent.style.padding = '20px';
- popupContent.style.borderRadius = '5px';
- popupContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
- popupContent.style.textAlign = 'center';
- const message = document.createElement('p');
- message.textContent = messageText;
- const yesButton = document.createElement('button');
- yesButton.textContent = 'Yes';
- yesButton.style.backgroundColor = '#C82800';
- yesButton.style.marginRight = '10px';
- yesButton.addEventListener('click', () => {
- onYes();
- document.body.removeChild(popupOverlay);
- });
- const noButton = document.createElement('button');
- noButton.textContent = 'No';
- noButton.addEventListener('click', () => {
- onNo();
- document.body.removeChild(popupOverlay);
- });
- popupContent.appendChild(message);
- popupContent.appendChild(yesButton);
- popupContent.appendChild(noButton);
- popupOverlay.appendChild(popupContent);
- document.body.appendChild(popupOverlay);
- }
- ////BUTTONS
- function createActionButtonsContainer() {
- const actionButtonWidth = '100px';
- const closeButton = createButton('Close', '10px', closeOverlayMenu, actionButtonWidth);
- const saveButton = createButton('Save', '10px', saveConfig, actionButtonWidth);
- const defaultButton = createDefaultButton(actionButtonWidth);
- const deleteButton = createDeleteButton(actionButtonWidth);
- const actionButtonsContainer = document.createElement('div');
- actionButtonsContainer.style.textAlign = 'center';
- actionButtonsContainer.style.marginTop = '10px';
- actionButtonsContainer.append(closeButton, saveButton, defaultButton, deleteButton);
- return actionButtonsContainer;
- }
- function createMenuButtons() {
- const blacklistContainer = createBlacklistContainer();
- const favoritesContainer = createFavoritesContainer();
- const dataContainer = createDataContainer();
- const actionButtonsContainer = createActionButtonsContainer();
- const buttonContainer = document.createElement('div');
- buttonContainer.append(blacklistContainer, favoritesContainer, dataContainer, actionButtonsContainer);
- return buttonContainer;
- }
- function createButton(text, margin, onClick, width) {
- // Create a button element with specified properties
- const button = document.createElement('button');
- button.textContent = text;
- button.style.margin = margin;
- button.style.width = width;
- button.style.textAlign = 'center';
- button.style.display = 'inline-block';
- button.style.lineHeight = '30px';
- button.style.padding = '5px 0';
- button.addEventListener('click', onClick);
- return button;
- }
- ////BLACKLIST BUTTONS
- function createBlacklistContainer() {
- const blacklistButtonWidth = '200px';
- const addBlacklistButton = createButton('Add to Blacklist', '10px', addBlacklist, blacklistButtonWidth);
- const remBlacklistButton = createButton('Remove from Blacklist', '10px', remBlacklist, blacklistButtonWidth);
- const blacklistContainer = document.createElement('div');
- blacklistContainer.style.textAlign = 'center';
- blacklistContainer.style.marginTop = '10px';
- blacklistContainer.append(addBlacklistButton, remBlacklistButton);
- return blacklistContainer;
- }
- ////FAVORITE BUTTONS
- function createFavoritesContainer() {
- const favoritesButtonWidth = '200px';
- const exportButton = createButton('Export Favorites', '10px', exportFavorites, favoritesButtonWidth);
- const importButton = createButton('Import Favorites', '10px', handleImportButtonClick, favoritesButtonWidth);
- const favoritesContainer = document.createElement('div');
- favoritesContainer.style.textAlign = 'center';
- favoritesContainer.style.marginTop = '10px';
- favoritesContainer.append(exportButton, importButton);
- return favoritesContainer;
- }
- ////DATA BUTTONS
- function createDataContainer() {
- const dataButtonWidth = '200px';
- const exportButton = createButton('Export Data', '10px', exportData, dataButtonWidth);
- const importButton = createButton('Import Data', '10px', handleImportDButtonClick, dataButtonWidth);
- const dataContainer = document.createElement('div');
- dataContainer.style.textAlign = 'center';
- dataContainer.style.marginTop = '10px';
- dataContainer.append(exportButton, importButton);
- return dataContainer;
- }
- ////CLOSE BUTTON
- function closeOverlayMenu() {
- loadConfig();
- document.body.removeChild(document.getElementById('overlayMenu'));
- }
- ////SAVE BUTTON
- function saveConfig() {
- const overlay = document.getElementById('overlayMenu');
- if (!overlay) return;
- const inputs = overlay.querySelectorAll('input, span');
- console.log(inputs)
- const {changes, minimumExampleLengthChanged, newMinimumExampleLength} = gatherChanges(inputs);
- if (minimumExampleLengthChanged) {
- handleMinimumExampleLengthChange(newMinimumExampleLength, changes);
- } else {
- applyChanges(changes);
- finalizeSaveConfig();
- setVocabSize();
- setPageWidth();
- }
- }
- function gatherChanges(inputs) {
- let minimumExampleLengthChanged = false;
- let newMinimumExampleLength;
- const changes = {};
- inputs.forEach(input => {
- const key = input.getAttribute('data-key');
- const type = input.getAttribute('data-type');
- let value;
- if (type === 'boolean') {
- value = input.checked;
- } else if (type === 'number') {
- value = parseFloat(input.textContent);
- } else if (type === 'string') {
- value = input.textContent;
- } else if (type === 'object' && key === 'HOTKEYS') {
- value = input.textContent.replace(' and ', ' ');
- }
- if (key && type) {
- const typePart = input.getAttribute('data-type-part');
- const originalFormattedType = typePart.slice(1, -1);
- if (key === 'MINIMUM_EXAMPLE_LENGTH' && CONFIG.MINIMUM_EXAMPLE_LENGTH !== value) {
- minimumExampleLengthChanged = true;
- newMinimumExampleLength = value;
- }
- if (key === 'MAXIMUM_EXAMPLE_LENGTH' && CONFIG.MAXIMUM_EXAMPLE_LENGTH !== value) {
- value = Math.max(value, CONFIG.MINIMUM_EXAMPLE_LENGTH);
- }
- changes[configPrefix + key] = value + originalFormattedType;
- }
- });
- return {changes, minimumExampleLengthChanged, newMinimumExampleLength};
- }
- function handleMinimumExampleLengthChange(newMinimumExampleLength, changes) {
- createConfirmationPopup(
- 'Changing Minimum Example Length will break your current favorites. They will all be deleted. Are you sure?',
- async () => {
- await IndexedDBManager.delete();
- CONFIG.MINIMUM_EXAMPLE_LENGTH = newMinimumExampleLength;
- setItem(`${configPrefix}MINIMUM_EXAMPLE_LENGTH`, newMinimumExampleLength);
- applyChanges(changes);
- clearNonConfigLocalStorage();
- finalizeSaveConfig();
- location.reload();
- },
- () => {
- const overlay = document.getElementById('overlayMenu');
- document.body.removeChild(overlay);
- document.body.appendChild(createOverlayMenu());
- }
- );
- }
- function clearNonConfigLocalStorage() {
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key && key.startsWith(scriptPrefix) && !key.startsWith(scriptPrefix + configPrefix)) {
- localStorage.removeItem(key);
- i--; // Adjust index after removal
- }
- }
- }
- function applyChanges(changes) {
- for (const key in changes) {
- setItem(key, changes[key]);
- }
- }
- function finalizeSaveConfig() {
- loadConfig();
- window.removeEventListener('keydown', hotkeysListener);
- renderImageAndPlayAudio(state.vocab, CONFIG.AUTO_PLAY_SOUND);
- const overlay = document.getElementById('overlayMenu');
- if (overlay) {
- document.body.removeChild(overlay);
- }
- }
- ////DEFAULT BUTTON
- function createDefaultButton(width) {
- const defaultButton = createButton('Default', '10px', () => {
- createConfirmationPopup(
- 'This will reset all your settings to default. Are you sure?',
- () => {
- Object.keys(localStorage).forEach(key => {
- if (key.startsWith(scriptPrefix + configPrefix)) {
- localStorage.removeItem(key);
- }
- });
- location.reload();
- },
- () => {
- const overlay = document.getElementById('overlayMenu');
- if (overlay) {
- document.body.removeChild(overlay);
- }
- loadConfig();
- document.body.appendChild(createOverlayMenu());
- }
- );
- }, width);
- defaultButton.style.backgroundColor = '#C82800';
- defaultButton.style.color = 'white';
- return defaultButton;
- }
- ////DELETE BUTTON
- function createDeleteButton(width) {
- const deleteButton = createButton('DELETE', '10px', () => {
- createConfirmationPopup(
- 'This will delete all your favorites and cached data. Are you sure?',
- async () => {
- await IndexedDBManager.delete();
- Object.keys(localStorage).forEach(key => {
- if (key.startsWith(scriptPrefix) && !key.startsWith(scriptPrefix + configPrefix)) {
- localStorage.removeItem(key);
- }
- });
- location.reload();
- },
- () => {
- const overlay = document.getElementById('overlayMenu');
- if (overlay) {
- document.body.removeChild(overlay);
- }
- loadConfig();
- document.body.appendChild(createOverlayMenu());
- }
- );
- }, width);
- deleteButton.style.backgroundColor = '#C82800';
- deleteButton.style.color = 'white';
- return deleteButton;
- }
- function createOverlayMenu() {
- // Create and return the overlay menu for configuration settings
- const overlay = document.createElement('div');
- overlay.id = 'overlayMenu';
- overlay.style.position = 'fixed';
- overlay.style.top = '0';
- overlay.style.left = '0';
- overlay.style.width = '100%';
- overlay.style.height = '100%';
- overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
- overlay.style.zIndex = '1000';
- overlay.style.display = 'flex';
- overlay.style.justifyContent = 'center';
- overlay.style.alignItems = 'center';
- const menuContent = document.createElement('div');
- menuContent.style.backgroundColor = 'var(--background-color)';
- menuContent.style.color = 'var(--text-color)';
- menuContent.style.padding = '20px';
- menuContent.style.borderRadius = '5px';
- menuContent.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)';
- menuContent.style.width = '80%';
- menuContent.style.maxWidth = '550px';
- menuContent.style.maxHeight = '80%';
- menuContent.style.overflowY = 'auto';
- for (const [key, value] of Object.entries(CONFIG)) {
- const optionContainer = document.createElement('div');
- optionContainer.style.marginBottom = '10px';
- optionContainer.style.display = 'flex';
- optionContainer.style.alignItems = 'center';
- const leftContainer = document.createElement('div');
- leftContainer.style.flex = '1';
- leftContainer.style.display = 'flex';
- leftContainer.style.alignItems = 'center';
- const rightContainer = document.createElement('div');
- rightContainer.style.flex = '1';
- rightContainer.style.display = 'flex';
- rightContainer.style.alignItems = 'center';
- rightContainer.style.justifyContent = 'center';
- const label = document.createElement('label');
- label.textContent = key.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
- label.style.marginRight = '10px';
- leftContainer.appendChild(label);
- if (key === 'RANDOM_SENTENCE') {
- const select = document.createElement('select');
- select.setAttribute('data-key', key);
- // Add options to the select dropdown for the enum values
- for (const [enumKey, enumValue] of Object.entries(RANDOM_SENTENCE_ENUM)) {
- const option = document.createElement('option');
- option.value = enumValue;
- option.text = enumKey.replace(/_/g, ' ').toLowerCase();
- option.selected = value === enumValue; // Set the current value as selected
- select.appendChild(option);
- }
- select.addEventListener('change', (event) => {
- CONFIG[key] = parseInt(event.target.value, 10); // Update the config with the selected value
- localStorage.setItem(`${scriptPrefix + configPrefix}${key}`, event.target.value); // Save to localStorage
- });
- rightContainer.appendChild(select);
- } else if (typeof value === 'boolean') {
- const checkboxContainer = document.createElement('div');
- checkboxContainer.style.display = 'flex';
- checkboxContainer.style.alignItems = 'center';
- checkboxContainer.style.justifyContent = 'center';
- const checkbox = document.createElement('input');
- checkbox.type = 'checkbox';
- checkbox.checked = value;
- checkbox.setAttribute('data-key', key);
- checkbox.setAttribute('data-type', 'boolean');
- checkbox.setAttribute('data-type-part', '');
- checkboxContainer.appendChild(checkbox);
- rightContainer.appendChild(checkboxContainer);
- } else if (typeof value === 'number') {
- const numberContainer = document.createElement('div');
- numberContainer.style.display = 'flex';
- numberContainer.style.alignItems = 'center';
- numberContainer.style.justifyContent = 'center';
- const decrementButton = document.createElement('button');
- decrementButton.textContent = '-';
- decrementButton.style.marginRight = '5px';
- const input = document.createElement('span');
- input.textContent = value;
- input.style.margin = '0 10px';
- input.style.minWidth = '3ch';
- input.style.textAlign = 'center';
- input.setAttribute('data-key', key);
- input.setAttribute('data-type', 'number');
- input.setAttribute('data-type-part', '');
- const incrementButton = document.createElement('button');
- incrementButton.textContent = '+';
- incrementButton.style.marginLeft = '5px';
- const updateButtonStates = () => {
- let currentValue = parseFloat(input.textContent);
- if (currentValue <= 0) {
- decrementButton.disabled = true;
- decrementButton.style.color = 'grey';
- } else {
- decrementButton.disabled = false;
- decrementButton.style.color = '';
- }
- if (key === 'SOUND_VOLUME' && currentValue >= 100) {
- incrementButton.disabled = true;
- incrementButton.style.color = 'grey';
- } else {
- incrementButton.disabled = false;
- incrementButton.style.color = '';
- }
- };
- decrementButton.addEventListener('click', () => {
- let currentValue = parseFloat(input.textContent);
- if (currentValue > 0) {
- if (currentValue > 200) {
- input.textContent = currentValue - 25;
- } else if (currentValue > 20) {
- input.textContent = currentValue - 5;
- } else {
- input.textContent = currentValue - 1;
- }
- updateButtonStates();
- }
- });
- incrementButton.addEventListener('click', () => {
- let currentValue = parseFloat(input.textContent);
- if (key === 'SOUND_VOLUME' && currentValue >= 100) {
- return;
- }
- if (currentValue >= 200) {
- input.textContent = currentValue + 25;
- } else if (currentValue >= 20) {
- input.textContent = currentValue + 5;
- } else {
- input.textContent = currentValue + 1;
- }
- updateButtonStates();
- });
- numberContainer.appendChild(decrementButton);
- numberContainer.appendChild(input);
- numberContainer.appendChild(incrementButton);
- rightContainer.appendChild(numberContainer);
- // Initialize button states
- updateButtonStates();
- } else if (typeof value === 'string') {
- const typeParts = value.split(/(\d+)/).filter(Boolean);
- const numberParts = typeParts.filter(part => !isNaN(part)).map(Number);
- const numberContainer = document.createElement('div');
- numberContainer.style.display = 'flex';
- numberContainer.style.alignItems = 'center';
- numberContainer.style.justifyContent = 'center';
- const typeSpan = document.createElement('span');
- const formattedType = '(' + typeParts.filter(part => isNaN(part)).join('').replace(/_/g, ' ').toLowerCase() + ')';
- typeSpan.textContent = formattedType;
- typeSpan.style.marginRight = '10px';
- leftContainer.appendChild(typeSpan);
- typeParts.forEach(part => {
- if (!isNaN(part)) {
- const decrementButton = document.createElement('button');
- decrementButton.textContent = '-';
- decrementButton.style.marginRight = '5px';
- const input = document.createElement('span');
- input.textContent = part;
- input.style.margin = '0 10px';
- input.style.minWidth = '3ch';
- input.style.textAlign = 'center';
- input.setAttribute('data-key', key);
- input.setAttribute('data-type', 'string');
- input.setAttribute('data-type-part', formattedType);
- const incrementButton = document.createElement('button');
- incrementButton.textContent = '+';
- incrementButton.style.marginLeft = '5px';
- const updateButtonStates = () => {
- let currentValue = parseFloat(input.textContent);
- if (currentValue <= 0) {
- decrementButton.disabled = true;
- decrementButton.style.color = 'grey';
- } else {
- decrementButton.disabled = false;
- decrementButton.style.color = '';
- }
- if (key === 'SOUND_VOLUME' && currentValue >= 100) {
- incrementButton.disabled = true;
- incrementButton.style.color = 'grey';
- } else {
- incrementButton.disabled = false;
- incrementButton.style.color = '';
- }
- };
- decrementButton.addEventListener('click', () => {
- let currentValue = parseFloat(input.textContent);
- if (currentValue > 0) {
- if (currentValue > 200) {
- input.textContent = currentValue - 25;
- } else if (currentValue > 20) {
- input.textContent = currentValue - 5;
- } else {
- input.textContent = currentValue - 1;
- }
- updateButtonStates();
- }
- });
- incrementButton.addEventListener('click', () => {
- let currentValue = parseFloat(input.textContent);
- if (key === 'SOUND_VOLUME' && currentValue >= 100) {
- return;
- }
- if (currentValue >= 200) {
- input.textContent = currentValue + 25;
- } else if (currentValue >= 20) {
- input.textContent = currentValue + 5;
- } else {
- input.textContent = currentValue + 1;
- }
- updateButtonStates();
- });
- numberContainer.appendChild(decrementButton);
- numberContainer.appendChild(input);
- numberContainer.appendChild(incrementButton);
- // Initialize button states
- updateButtonStates();
- }
- });
- rightContainer.appendChild(numberContainer);
- } else if (typeof value === 'object') {
- const maxAllowedIndex = hotkeyOptions.length - 1
- let currentValue = value;
- let choiceIndex = hotkeyOptions.indexOf(currentValue.join(' '));
- if (choiceIndex === -1) {
- currentValue = hotkeyOptions[0].split(' ');
- choiceIndex = 0;
- }
- const textContainer = document.createElement('div');
- textContainer.style.display = 'flex';
- textContainer.style.alignItems = 'center';
- textContainer.style.justifyContent = 'center';
- const decrementButton = document.createElement('button');
- decrementButton.textContent = '<';
- decrementButton.style.marginRight = '5px';
- const input = document.createElement('span');
- input.textContent = currentValue.join(' and ');
- input.style.margin = '0 10px';
- input.style.minWidth = '3ch';
- input.style.textAlign = 'center';
- input.setAttribute('data-key', key);
- input.setAttribute('data-type', 'object');
- input.setAttribute('data-type-part', '');
- const incrementButton = document.createElement('button');
- incrementButton.textContent = '>';
- incrementButton.style.marginLeft = '5px';
- const updateButtonStates = () => {
- if (choiceIndex <= 0) {
- decrementButton.disabled = true;
- decrementButton.style.color = 'grey';
- } else {
- decrementButton.disabled = false;
- decrementButton.style.color = '';
- }
- if (choiceIndex >= maxAllowedIndex) {
- incrementButton.disabled = true;
- incrementButton.style.color = 'grey';
- } else {
- incrementButton.disabled = false;
- incrementButton.style.color = '';
- }
- };
- decrementButton.addEventListener('click', () => {
- if (choiceIndex > 0) {
- choiceIndex -= 1;
- currentValue = hotkeyOptions[choiceIndex].split(' ');
- input.textContent = currentValue.join(' and ');
- updateButtonStates();
- }
- });
- incrementButton.addEventListener('click', () => {
- if (choiceIndex < maxAllowedIndex) {
- choiceIndex += 1;
- currentValue = hotkeyOptions[choiceIndex].split(' ');
- input.textContent = currentValue.join(' and ');
- updateButtonStates();
- }
- });
- textContainer.appendChild(decrementButton);
- textContainer.appendChild(input);
- textContainer.appendChild(incrementButton);
- // Initialize button states
- updateButtonStates();
- rightContainer.appendChild(textContainer);
- }
- optionContainer.appendChild(leftContainer);
- optionContainer.appendChild(rightContainer);
- menuContent.appendChild(optionContainer);
- }
- const menuButtons = createMenuButtons();
- menuContent.appendChild(menuButtons);
- overlay.appendChild(menuContent);
- return overlay;
- }
- function loadConfig() {
- for (const key in localStorage) {
- if (!key.startsWith(scriptPrefix + configPrefix) || !localStorage.hasOwnProperty(key)) {
- continue
- }
- const configKey = key.substring((scriptPrefix + configPrefix).length); // chop off script prefix and config prefix
- if (!CONFIG.hasOwnProperty(configKey)) {
- continue
- }
- const savedValue = localStorage.getItem(key);
- if (savedValue === null) {
- continue
- }
- const valueType = typeof CONFIG[configKey];
- if (configKey === 'RANDOM_SENTENCE') {
- if (savedValue == 0) {
- CONFIG[configKey] = RANDOM_SENTENCE_ENUM.DISABLE
- }
- if (savedValue == 1) {
- CONFIG[configKey] = RANDOM_SENTENCE_ENUM.ON_FIRST
- }
- if (savedValue == 2) {
- CONFIG[configKey] = RANDOM_SENTENCE_ENUM.EVERY_TIME
- }
- } else if (configKey === 'HOTKEYS') {
- CONFIG[configKey] = savedValue.split(' ')
- } else if (valueType === 'boolean') {
- CONFIG[configKey] = savedValue === 'true';
- if (configKey === 'DEFAULT_TO_EXACT_SEARCH') {
- state.exactSearch = CONFIG.DEFAULT_TO_EXACT_SEARCH
- }
- // I wonder if this is the best way to do this...
- // Probably not because we could just have a single variable to store both, but it would have to be in config and
- // it would be a bit weird to have the program modifying config when the actual config settings aren't changing
- } else if (valueType === 'number') {
- CONFIG[configKey] = parseFloat(savedValue);
- } else if (valueType === 'string') {
- CONFIG[configKey] = savedValue;
- }
- }
- }
- function checkVocabInSentence(state, sentence) {
- // Create the payload using the sentence content and required fields
- const payload = {
- input: sentence.segment_info.content_jp,
- };
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "POST",
- url: "https://kanjikana.com/api/furigana",
- data: JSON.stringify(payload),
- onload: function (response) {
- // Ensure a 200 OK response
- if (response.status === 200) {
- const result = JSON.parse(response.responseText)["furigana"];
- let foundMatch = false;
- function parseRubyToJSON(text) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(text, "text/html");
- const rubyElements = doc.querySelectorAll("ruby");
- let result = Array.from(rubyElements).map(ruby => {
- const word = ruby.childNodes[0].textContent; // The kanji part
- const reading = ruby.querySelector("rt").textContent; // The ruby (furigana)
- return {word, reading};
- });
- // Check for words without ruby
- const nonRubyElements = Array.from(doc.body.childNodes).filter(
- node => node.nodeType === Node.TEXT_NODE && node.textContent.trim()
- );
- nonRubyElements.forEach(nonRuby => {
- result.push({word: nonRuby.textContent.trim(), reading: null});
- });
- return result;
- }
- const jsonResult = parseRubyToJSON(result);
- jsonResult.forEach(function (token) {
- // Destructure the token array
- const [spelling, reading] = [token.word, token.reading];
- // Check if the token's spelling matches our desired vocab
- // and its reading matches the expected reading (from state)
- if (spelling === state.vocab && reading === state.reading) {
- foundMatch = true;
- } else if (spelling === state.vocab) {
- console.log(`Reading mismatch: ${spelling} - Expected: ${state.reading}, Found: ${reading}`);
- }
- });
- resolve(foundMatch);
- } else {
- console.error("API call failed with status", response.status);
- reject(new Error("API call failed"));
- }
- },
- onerror: function (error) {
- console.error("Error during the API call:", error);
- reject(error);
- }
- });
- })
- }
- async function process_sentences(state, sentences, first_call) {
- // Early return for empty array or single item (no processing needed)
- if (!sentences || !Array.isArray(sentences) || sentences.length <= 1) {
- return sentences;
- }
- // Only randomize if needed
- const shouldRandomize = CONFIG.RANDOM_SENTENCE >
- (first_call ? RANDOM_SENTENCE_ENUM.DISABLE : RANDOM_SENTENCE_ENUM.ON_FIRST);
- // Skip weight calculation if not needed
- if (!CONFIG.WEIGHTED_SENTENCES && !shouldRandomize) {
- return sentences;
- }
- // Set weights for each sentence by calling jpdb api (if needed)
- if (CONFIG.WEIGHTED_SENTENCES && jpdbApiKey) {
- // Create batches of API calls to reduce network overhead
- const BATCH_SIZE = 5; // Process 5 sentences at a time
- const sentencesToProcess = [];
- // Filter only sentences that need processing (not in cache)
- for (let i = 0; i < sentences.length; i++) {
- const sentence = sentences[i];
- const content = sentence.segment_info?.content_jp;
- if (!content) continue;
- sentencesToProcess.push({sentence, index: i});
- }
- // Process in batches
- for (let i = 0; i < sentencesToProcess.length; i += BATCH_SIZE) {
- const batch = sentencesToProcess.slice(i, i + BATCH_SIZE);
- const batchPromises = batch.map(({sentence, index}) => {
- const content = sentence.segment_info.content_jp;
- const data = {
- "text": content,
- "token_fields": [],
- "position_length_encoding": "utf16",
- "vocabulary_fields": [
- "card_state", "spelling"
- ]
- };
- if (sentence.nulled) {
- sentences[index].weight = 1e-6;
- console.log(`Ignoring "${content}" due to null`);
- return Promise.resolve();
- }
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "POST",
- url: "https://jpdb.io/api/v1/parse",
- headers: {
- "Authorization": `Bearer ${jpdbApiKey}`,
- },
- data: JSON.stringify(data),
- onload: function (response) {
- if (response.status === 200) {
- try {
- const result = JSON.parse(response.responseText);
- const vocabulary = result.vocabulary || [];
- const amount = vocabulary.length;
- if (amount === 0) {
- sentences[index].weight = 1;
- return resolve();
- }
- const VALID_CARD_STATES = ["known", "never-forget", "learning"];
- let weight = 0;
- // Use faster loop construct
- for (let j = 0; j < amount; j++) {
- const vocabItem = vocabulary[j];
- if (vocabItem && vocabItem[0] &&
- VALID_CARD_STATES.includes(vocabItem[0][0])) {
- weight++;
- }
- }
- // Calculate and store weight
- sentences[index].weight = (weight * 100 / ((amount * amount))) || 1;
- resolve();
- } catch (e) {
- // Default weight on error
- sentences[index].weight = 1;
- resolve();
- }
- } else {
- // Default weight on error
- sentences[index].weight = 1;
- resolve();
- }
- },
- onerror: function () {
- // Default weight on error
- sentences[index].weight = 1;
- resolve();
- }
- });
- });
- });
- // Wait for current batch to complete before moving to next
- await Promise.all(batchPromises);
- }
- }
- // Randomize sentences if needed
- if (shouldRandomize) {
- // Use Fisher-Yates shuffle for better performance
- // Only shuffle a maximum of 50 items for large arrays to improve performance
- const maxShuffleItems = Math.min(sentences.length, 50);
- // Optimized weighted random algorithm
- const getWeightedRandomIndex = (max) => {
- // Pre-calculate weights array for performance
- const weights = new Array(max);
- let totalWeight = 0;
- for (let i = 0; i < max; i++) {
- const weight = (sentences[i].weight || 1);
- weights[i] = weight;
- totalWeight += weight;
- }
- // Get random value proportional to total weight
- const random = Math.random() * totalWeight;
- let cumulativeWeight = 0;
- // Find the index
- for (let i = 0; i < max; i++) {
- cumulativeWeight += weights[i];
- if (random <= cumulativeWeight) {
- return i;
- }
- }
- return max - 1; // Fallback
- };
- // Fisher-Yates shuffle with weighted randomization
- for (let i = maxShuffleItems - 1; i > 0; i--) {
- const j = CONFIG.WEIGHTED_SENTENCES ?
- getWeightedRandomIndex(i + 1) :
- Math.floor(Math.random() * (i + 1));
- // Swap elements
- if (i !== j) {
- [sentences[i], sentences[j]] = [sentences[j], sentences[i]];
- }
- }
- }
- return sentences;
- }
- //MAIN FUNCTIONS=====================================================================================================================
- async function onPageLoad() {
- // Initialize state and determine vocabulary based on URL
- state.embedAboveSubsectionMeanings = false;
- // Early layout adjustments without waiting
- setPageWidth();
- const sentenceElement = document.querySelector('.sentence');
- if (sentenceElement) {
- sentenceElement.textContent = "Waiting for data...";
- }
- const machineTranslationFrame = document.getElementById('machine-translation-frame');
- // Skip if machine translation frame is present
- if (machineTranslationFrame) return;
- // Determine the vocabulary based on URL — done in parallel with setting page width
- const url = window.location.href;
- if (url.includes('/vocabulary/')) {
- [state.vocab, state.reading] = parseVocabFromVocabulary();
- } else if (url.includes('/search?q=')) {
- state.vocab = parseVocabFromSearch();
- } else if (url.includes('c=')) {
- state.vocab = parseVocabFromAnswer();
- } else if (url.includes('/kanji/')) {
- state.vocab = parseVocabFromKanji();
- } else {
- [state.vocab, state.reading] = parseVocabFromReview();
- }
- // Early return if no vocabulary is found
- if (!state.vocab) return;
- // Retrieve stored data for the current vocabulary
- const {sentence, exactState} = getStoredData(state.vocab);
- state.exactSearch = exactState;
- // Fetch data if needed, process in parallel threads where possible
- if (!state.apiDataFetched) {
- try {
- await getNadeshikoData(state.vocab, state.exactSearch);
- // Process sentences in parallel with preloading images
- const processingPromise = process_sentences(state, state.examples, true);
- const preloadPromise = Promise.resolve().then(() => preloadImages());
- state.examples = await processingPromise;
- // Set current example index if a sentence exists
- if (sentence) {
- state.currentExampleIndex = state.examples.findIndex(
- example => example.segment_info.content_jp === sentence
- );
- }
- // Wait for preloading to complete
- await preloadPromise;
- // Finally, display the example
- embedImageAndPlayAudio();
- } catch (error) {
- // Handle errors silently for better performance
- state.error = true;
- embedImageAndPlayAudio(); // Still try to show what we can
- }
- } else if (state.apiDataFetched) {
- // Data already fetched, just update display
- if (sentence) {
- // Process sentence index finding without logging
- state.currentExampleIndex = await process_sentences(
- state,
- state.examples.findIndex(example => example.segment_info.content_jp === sentence),
- false
- );
- }
- // Update display and settings
- Promise.all([
- Promise.resolve().then(() => embedImageAndPlayAudio()),
- Promise.resolve().then(() => setVocabSize())
- ]);
- }
- }
- function setPageWidth() {
- // Set the maximum width of the page
- document.body.style.maxWidth = CONFIG.PAGE_WIDTH;
- }
- // Observe URL changes and reload the page content accordingly
- const observer = new MutationObserver(() => {
- if (window.location.href !== observer.lastUrl) {
- observer.lastUrl = window.location.href;
- onPageLoad();
- }
- });
- // Function to apply styles
- function setVocabSize() {
- // Create a new style element
- const style = document.createElement('style');
- style.type = 'text/css';
- style.innerHTML = `
- .answer-box > .plain {
- font-size: ${CONFIG.VOCAB_SIZE} !important; /* Use the configurable font size */
- padding-bottom: 0.1rem !important; /* Retain padding */
- }
- `;
- // Append the new style to the document head
- document.head.appendChild(style);
- }
- observer.lastUrl = window.location.href;
- observer.observe(document, {subtree: true, childList: true});
- // Add event listeners for page load and URL changes
- window.addEventListener('load', onPageLoad);
- window.addEventListener('popstate', onPageLoad);
- window.addEventListener('hashchange', onPageLoad);
- // Initial configuration and preloading
- loadConfig();
- setPageWidth();
- setVocabSize();
- //preloadImages();
- })();