您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Embeds anime images & audio examples into JPDB review and vocabulary pages using Immersion Kit's API.
- // ==UserScript==
- // @name JPDB Immersion Kit Examples
- // @version 1.24
- // @description Embeds anime images & audio examples into JPDB review and vocabulary pages using Immersion Kit's API.
- // @author awoo
- // @namespace jpdb-immersion-kit-examples
- // @match https://jpdb.io/review*
- // @match https://jpdb.io/vocabulary/*
- // @match https://jpdb.io/kanji/*
- // @match https://jpdb.io/search*
- // @connect immersionkit.com
- // @connect linodeobjects.com
- // @grant GM_addElement
- // @grant GM_xmlhttpRequest
- // @license MIT
- // ==/UserScript==
- (function() {
- 'use strict';
- // 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 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,
- 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
- };
- const state = {
- currentExampleIndex: 0,
- examples: [],
- apiDataFetched: false,
- vocab: '',
- embedAboveSubsectionMeanings: false,
- preloadedIndices: new Set(),
- currentAudio: null,
- exactSearch: true,
- error: false,
- currentlyPlayingAudio: false,
- sharedAudioContext: new (window.AudioContext || window.webkitAudioContext)(),
- currentSource: null,
- lastPlayId: 0,
- };
- // Prefixing
- const scriptPrefix = 'JPDBImmersionKitExamples-';
- 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(`JPDBImmersionKit*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(`JPDBImmersionKit*Examples-CONFIG_VARIABLES_PREFIXED`, 'true');
- }
- // IndexedDB Manager
- const IndexedDBManager = {
- DB_NAME: 'ImmersionKitDB',
- DB_VERSION: 2, // bump version to create metaStore
- DATA_STORE: 'dataStore',
- META_STORE: 'metaStore',
- META_KEY: 'index_meta',
- MAX_ENTRIES: 100000000,
- EXPIRATION_TIME: 30 * 24 * 60 * 60 * 1000 * 12 * 10000, // 10000 years
- open() {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
- request.onupgradeneeded = event => {
- const db = event.target.result;
- if (!db.objectStoreNames.contains(this.DATA_STORE)) {
- db.createObjectStore(this.DATA_STORE, { keyPath: 'keyword' });
- }
- if (!db.objectStoreNames.contains(this.META_STORE)) {
- db.createObjectStore(this.META_STORE, { keyPath: 'key' });
- }
- };
- request.onsuccess = event => resolve(event.target.result);
- request.onerror = event => reject('IndexedDB error: ' + event.target.errorCode);
- });
- },
- // ---------- META STORE ----------
- getMetadata(db) {
- return new Promise((resolve, reject) => {
- const tx = db.transaction([this.META_STORE], 'readonly');
- const store = tx.objectStore(this.META_STORE);
- const req = store.get(this.META_KEY);
- req.onsuccess = e => {
- const rec = e.target.result;
- resolve(rec ? rec.data : null);
- };
- req.onerror = e => reject('Failed to read metadata: ' + e.target.errorCode);
- });
- },
- saveMetadata(db, metadata) {
- return new Promise((resolve, reject) => {
- const tx = db.transaction([this.META_STORE], 'readwrite');
- const store = tx.objectStore(this.META_STORE);
- const rec = { key: this.META_KEY, data: metadata, timestamp: Date.now() };
- const req = store.put(rec);
- req.onsuccess = () => resolve();
- req.onerror = e => reject('Failed to save metadata: ' + e.target.errorCode);
- });
- },
- // ---------- DATA STORE ----------
- get(db, keyword) {
- return new Promise((resolve, reject) => {
- const tx = db.transaction([this.DATA_STORE], 'readonly');
- const store = tx.objectStore(this.DATA_STORE);
- const req = store.get(keyword);
- req.onsuccess = async e => {
- const result = e.target.result;
- if (!result) return resolve(null);
- // Return the data field directly
- resolve(result.data ? result.data : result);
- };
- req.onerror = e => reject('IndexedDB get error: ' + e.target.errorCode);
- });
- },
- deleteEntry(db, keyword) {
- return new Promise((resolve, reject) => {
- const tx = db.transaction([this.DATA_STORE], 'readwrite');
- const store = tx.objectStore(this.DATA_STORE);
- const req = store.delete(keyword);
- req.onsuccess = () => resolve();
- req.onerror = e => reject('IndexedDB delete error: ' + e.target.errorCode);
- });
- },
- getAll(db) {
- return new Promise((resolve, reject) => {
- const tx = db.transaction([this.DATA_STORE], 'readonly');
- const store = tx.objectStore(this.DATA_STORE);
- const entries = [];
- store.openCursor().onsuccess = e => {
- const cursor = e.target.result;
- if (cursor) {
- entries.push(cursor.value);
- cursor.continue();
- } else {
- resolve(entries);
- }
- };
- store.openCursor().onerror = e => reject('Cursor error: ' + e.target.errorCode);
- });
- },
- // Fallback network fetch
- fetchMetadata() {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: "https://apiv2.immersionkit.com/index_meta",
- onload: res => {
- if (res.status === 200) {
- try {
- resolve(JSON.parse(res.responseText));
- } catch (err) {
- reject('Invalid JSON: ' + err);
- }
- } else {
- reject('HTTP ' + res.status);
- }
- },
- onerror: err => reject('Network error: ' + err)
- });
- });
- },
- save(db, keyword, data) {
- return new Promise(async (resolve, reject) => {
- try {
- const validationError = validateApiResponse(data);
- if (validationError) {
- console.warn(`Invalid data: ${validationError}`);
- return resolve();
- }
- // 1) load metadata from DB (or fetch & save if missing)
- let metadata = await this.getMetadata(db);
- if (!metadata) {
- metadata = await this.fetchMetadata();
- await this.saveMetadata(db, metadata);
- }
- // 2) slim down
- const slimData = {};
- if (data.category_count) slimData.category_count = data.category_count;
- if (!Array.isArray(data.examples)) {
- console.error('Unexpected examples format');
- return resolve();
- }
- // 3) map & patch titles
- const categoryOrder = ['anime', 'drama', 'games', 'literature', 'news'];
- let slimExamples = await Promise.all(data.examples.map(async ex => {
- const slim = {
- image: ex.image,
- sound: ex.sound,
- sentence: ex.sentence,
- translation: ex.translation,
- title: ex.title,
- media: ex.id ? ex.id.split('_')[0] : undefined
- };
- // if title not in our local metadata, re-fetch & update
- if (!metadata.data[slim.title]) {
- metadata = await this.fetchMetadata();
- await this.saveMetadata(db, metadata);
- }
- const entry = metadata.data[slim.title];
- if (entry) slim.title = entry.title;
- return slim;
- }));
- // Check state.exactSearch and filter examples
- console.log('State exactSearch:', state.exactSearch);
- console.log('State vocab:', state.vocab);
- const totalExamplesBefore = slimExamples.length;
- console.log('Total examples before filtering:', totalExamplesBefore);
- if (state.exactSearch) {
- const initialCount = slimExamples.length;
- slimExamples = slimExamples.filter(ex => ex.sentence.includes(state.vocab));
- const removedCount = initialCount - slimExamples.length;
- console.log('Number of examples removed:', removedCount);
- }
- const totalExamplesAfter = slimExamples.length;
- console.log('Total examples after filtering:', totalExamplesAfter);
- // 4) sort
- slimExamples.sort((a, b) => {
- const ca = categoryOrder.indexOf(a.media);
- const cb = categoryOrder.indexOf(b.media);
- if (ca !== cb) return ca - cb;
- return a.sentence.length - b.sentence.length;
- });
- slimData.examples = slimExamples;
- // 5) enforce MAX_ENTRIES
- const all = await this.getAll(db);
- const tx = db.transaction([this.DATA_STORE], 'readwrite');
- const store = tx.objectStore(this.DATA_STORE);
- if (all.length >= this.MAX_ENTRIES) {
- all
- .sort((a, b) => a.timestamp - b.timestamp)
- .slice(0, all.length - this.MAX_ENTRIES + 1)
- .forEach(old => store.delete(old.keyword));
- }
- // 6) put new record
- store.put({ keyword, data: slimData, timestamp: Date.now() });
- tx.oncomplete = () => {
- console.log('Save complete');
- resolve();
- };
- tx.onerror = e => reject('Save transaction failed: ' + e.target.errorCode);
- } catch (err) {
- reject('Error in save(): ' + err);
- }
- });
- },
- async versionupdate(db, searchVocab) {
- return new Promise(async (resolve, reject) => {
- try {
- // Fetch the existing data for the given searchVocab
- let cachedData = await this.get(db, searchVocab);
- if (!cachedData || !cachedData.data) {
- return resolve(); // No data to update
- }
- // Check if cachedData.data is an array and extract the first element
- const dataToTransform = Array.isArray(cachedData.data) ? cachedData.data[0] : cachedData.data;
- // Transform the data
- const updatedData = {
- category_count: dataToTransform.category_count,
- examples: dataToTransform.examples.map(example => {
- const imageUrlParts = example.image_url.split('/');
- const soundUrlParts = example.sound_url.split('/');
- // Extract media from the URL
- const mediaIndex = imageUrlParts.indexOf('media');
- const media = mediaIndex !== -1 && mediaIndex + 1 < imageUrlParts.length ? imageUrlParts[mediaIndex + 1] : '';
- return {
- image: imageUrlParts[imageUrlParts.length - 1],
- sound: soundUrlParts[soundUrlParts.length - 1],
- sentence: example.sentence,
- translation: example.translation,
- title: example.deck_name,
- media: media,
- };
- })
- };
- // Open a readwrite transaction on your data store
- const tx = db.transaction([this.DATA_STORE], 'readwrite');
- const store = tx.objectStore(this.DATA_STORE);
- // Put the new record directly
- store.put({
- keyword: searchVocab,
- data: updatedData,
- timestamp: Date.now()
- });
- tx.oncomplete = () => {
- console.log('Version update complete');
- resolve();
- };
- tx.onerror = e => {
- console.error('Version update transaction failed:', e.target.errorCode);
- reject('Version update transaction failed: ' + e.target.errorCode);
- };
- } catch (error) {
- console.error('Error in versionupdate:', error);
- reject('Error in versionupdate: ' + error);
- }
- });
- }
- ,
- delete() {
- return new Promise((resolve, reject) => {
- const req = indexedDB.deleteDatabase(this.DB_NAME);
- req.onsuccess = () => resolve();
- req.onerror = e => reject('Delete failed: ' + e.target.errorCode);
- req.onblocked = () => reject('Delete blocked; close all other tabs');
- });
- }
- };
- // API FUNCTIONS=====================================================================================================================
- function getImmersionKitData(vocab, exactSearch) {
- return new Promise(async (resolve, reject) => {
- const searchVocab = exactSearch ? `「${vocab}」` : vocab;
- const url = `https://apiv2.immersionkit.com/search?q=${encodeURIComponent(searchVocab)}`;
- 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();
- let cachedData = await IndexedDBManager.get(db, searchVocab);
- // Check if the cached data is outdated (v1 API data with 'data' field as an array)
- if (cachedData && Array.isArray(cachedData.data) && cachedData.data.length > 0) {
- console.log('Outdated data detected, updating...');
- await IndexedDBManager.versionupdate(db, searchVocab);
- // Rerun fetchData after updating
- return fetchData();
- } else if (cachedData && cachedData.examples && Array.isArray(cachedData.examples) && cachedData.examples.length > 0) {
- console.log('Data retrieved from IndexedDB');
- state.examples = cachedData.examples;
- state.apiDataFetched = true;
- updateCurrentExampleIndex();
- resolve();
- } else {
- console.log(`Calling API for: ${searchVocab}`);
- GM_xmlhttpRequest({
- method: "GET",
- url: url,
- onload: async function(response) {
- if (response.status === 200) {
- const jsonData = parseJSON(response.responseText);
- console.log("API JSON Received");
- console.log(url);
- const validationError = validateApiResponse(jsonData);
- if (!validationError) {
- await IndexedDBManager.save(db, searchVocab, jsonData);
- // Attempt to load the data from cache again after saving
- cachedData = await IndexedDBManager.get(db, searchVocab);
- if (cachedData && cachedData.examples && Array.isArray(cachedData.examples)) {
- console.log('Data retrieved from IndexedDB after saving');
- state.examples = cachedData.examples;
- state.apiDataFetched = true;
- updateCurrentExampleIndex();
- resolve();
- } else {
- reject('Failed to retrieve data from IndexedDB after saving');
- }
- } else {
- attempt++;
- if (attempt < maxRetries) {
- console.log(`Validation error: ${validationError}. Retrying... (${attempt}/${maxRetries})`);
- setTimeout(fetchData, 2000); // Add a 2-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}`);
- }
- }
- function updateCurrentExampleIndex() {
- const storedValue = getItem(state.vocab);
- if (storedValue) {
- // If stored data exists, use it to update the current example index
- const storedIndex = parseInt(storedValue, 10);
- // Update the current example index with the stored index
- state.currentExampleIndex = storedIndex;
- return;
- }
- // If no stored data exists, check sentence length
- for (let i = 0; i < state.examples.length; i++) {
- if (state.examples[i].sentence.length >= CONFIG.MINIMUM_EXAMPLE_LENGTH) {
- state.currentExampleIndex = i;
- break;
- }
- }
- }
- 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';
- }
- if (!jsonData.category_count || !jsonData.examples) {
- return 'Missing required data fields';
- }
- const categoryCount = jsonData.category_count;
- if (!categoryCount || Object.keys(categoryCount).length === 0) {
- return 'Missing or empty category count';
- }
- // Check if all category counts are zero
- const allZero = Object.values(categoryCount).every(count => count === 0);
- if (allZero) {
- return 'Blank API';
- }
- const examples = jsonData.examples;
- if (!Array.isArray(examples) || examples.length === 0) {
- return 'Missing or empty examples array';
- }
- 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, index, exactState) {
- // Create a string value from index and exactState to store in localStorage
- const value = `${index},${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 '';
- }
- let vocabulary = plainElement.textContent.trim();
- const nestedVocabularyElement = plainElement.querySelector('div:not([style])');
- if (nestedVocabularyElement) {
- vocabulary = nestedVocabularyElement.textContent.trim();
- }
- const specificVocabularyElement = plainElement.querySelector('div:nth-child(3)');
- if (specificVocabularyElement) {
- vocabulary = specificVocabularyElement.textContent.trim();
- }
- // 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;
- }
- } 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
- return decodeURIComponent(vocab);
- }
- // 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();
- getImmersionKitData(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) {
- const textButton = document.createElement('a');
- textButton.textContent = 'Immersion Kit';
- textButton.style.color = 'var(--subsection-label-color)';
- textButton.style.fontSize = '85%';
- textButton.style.marginRight = '0.5rem';
- textButton.style.verticalAlign = 'middle';
- const url = new URL('https://www.immersionkit.com/dictionary');
- url.searchParams.set('keyword', vocab);
- url.searchParams.set('sort', 'sentence_length:asc');
- if (exact) url.searchParams.set('exact', 'true');
- textButton.href = url.toString();
- 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;
- }
- // ——— Stop any playing audio ———
- function stopCurrentAudio() {
- if (state.currentSource) {
- try {
- state.currentSource.onended = null;
- state.currentSource.stop(0);
- state.currentSource.disconnect();
- } catch (e) { /* already stopped? ignore */ }
- state.currentSource = null;
- }
- }
- // ——— Play a new clip ———
- function playAudio(soundUrl) {
- if (!soundUrl) return;
- // 1) bump play ID to cancel any in-flight requests/decodes
- const playId = ++state.lastPlayId;
- // 2) tear down old source instantly
- stopCurrentAudio();
- // 3) ensure context is resumed (autoplay policy)
- if (state.sharedAudioContext.state === 'suspended') {
- state.sharedAudioContext.resume().catch(() => {});
- }
- // 4) fetch via GM_xmlhttpRequest
- GM_xmlhttpRequest({
- method: 'GET',
- url: soundUrl,
- responseType: 'arraybuffer',
- onload(response) {
- // if a newer playAudio() ran, abort
- if (playId !== state.lastPlayId) return;
- state.sharedAudioContext.decodeAudioData(
- response.response,
- buffer => {
- if (playId !== state.lastPlayId) return;
- // wire up new source + gain
- const src = state.sharedAudioContext.createBufferSource();
- src.buffer = buffer;
- const gain = state.sharedAudioContext.createGain();
- gain.gain.setValueAtTime(0, state.sharedAudioContext.currentTime);
- gain.gain.linearRampToValueAtTime(
- (CONFIG.SOUND_VOLUME || 100) / 100,
- state.sharedAudioContext.currentTime + 0.05
- );
- src.connect(gain).connect(state.sharedAudioContext.destination);
- // start (skip initial 50 ms to avoid pop)
- src.start(0, 0.05);
- // when it ends, clear the reference if still “ours”
- src.onended = () => {
- if (state.currentSource === src) {
- state.currentSource = null;
- }
- };
- // hold onto it so stopCurrentAudio() can find it
- state.currentSource = src;
- },
- err => {
- console.error('decodeAudioData failed:', err);
- }
- );
- },
- onerror(err) {
- console.error('GM_xmlhttpRequest failed:', err);
- }
- });
- }
- // has to be declared (referenced in multiple functions but definition requires variables local to one function)
- let hotkeysListener;
- function renderImageAndPlayAudio(vocab, shouldAutoPlaySound) {
- const example = state.examples[state.currentExampleIndex] || {};
- const imageUrl = example.image ? `https://us-southeast-1.linodeobjects.com/immersionkit/media/${example.media}/${example.title}/media/${example.image}` : null;
- const soundUrl = example.sound ? `https://us-southeast-1.linodeobjects.com/immersionkit/media/${example.media}/${example.title}/media/${example.sound}` : null;
- const sentence = example.sentence || null;
- const translation = example.translation || null;
- const title = example.title || null;
- const storedValue = getItem(state.vocab);
- const isBlacklisted = storedValue && storedValue.split(',').length > 1 && parseInt(storedValue.split(',')[1], 10) === 2;
- // 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(${title})`));
- }
- // Append sentence and translation or a placeholder text
- sentence ? appendSentenceAndTranslation(wrapperDiv, sentence, translation) : appendNoneText(wrapperDiv);
- } else if (state.error) {
- wrapperDiv.appendChild(createTextElement('ERROR\nNO EXAMPLES FOUND\n\nRARE WORD OR\nIMMERSIONKIT 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('immersion-kit-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;
- }
- // Detect iOS
- function isIOS() {
- return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
- }
- // Preload images
- 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) {
- const example = state.examples[i];
- const imageUrl = `https://us-southeast-1.linodeobjects.com/immersionkit/media/${example.media}/${example.title}/media/${example.image}`;
- if (isIOS()) {
- GM_xmlhttpRequest({
- method: 'GET',
- url: imageUrl,
- responseType: 'blob',
- onload: function(response) {
- if (response.status === 200 && response.response) {
- example.blob = response.response;
- state.preloadedIndices.add(i);
- }
- }
- });
- } else {
- GM_addElement(preloadDiv, 'img', { src: imageUrl });
- state.preloadedIndices.add(i);
- }
- }
- }
- }
- // Create image element
- function createImageElement(wrapperDiv, imageUrl, vocab, exactSearch) {
- const searchVocab = exactSearch ? `「${vocab}」` : vocab;
- const example = state.examples[state.currentExampleIndex] || {};
- const title = example.title || '';
- let file_name = imageUrl.substring(imageUrl.lastIndexOf('/') + 1).replace(/^(Anime_|A_|Z)/, '');
- const titleText = `${searchVocab} #${state.currentExampleIndex + 1}\n${title}\n${file_name}`;
- if (isIOS()) {
- // --- Calculate width and 16:9 height from config ---
- const width = parseInt(CONFIG.IMAGE_WIDTH, 10);
- const height = Math.round(width * 9 / 16);
- // --- Outer container ---
- const imgContainer = document.createElement('div');
- imgContainer.style = `width:${width}px;max-width:${width}px;margin:10px auto 0;position:relative;min-height:${height}px;`;
- // --- Hidden image until loaded ---
- const img = document.createElement('img');
- img.alt = 'Embedded Image';
- img.title = titleText;
- img.style = `width:100%;max-width:${width}px;margin-top:10px;cursor:pointer;display:none;border-radius:4px;height:auto;`;
- // --- Error fallback, also 16:9 ---
- const errorFallback = document.createElement('div');
- errorFallback.style = `display:none;width:100%;aspect-ratio:16/9;`;
- errorFallback.innerHTML =
- `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
- <rect width="${width}" height="${height}" fill="#f8d7da"/>
- <text x="50%" y="50%" text-anchor="middle" fill="#721c24" dy=".3em" font-size="18">Image failed to load</text>
- </svg>`;
- imgContainer.append(img, errorFallback);
- wrapperDiv.appendChild(imgContainer);
- // --- Use cached blob else load ---
- if (example.blob) {
- const objectURL = URL.createObjectURL(example.blob);
- img.src = objectURL;
- img.onload = () => {
- errorFallback.style.display = 'none';
- img.style.display = 'block';
- URL.revokeObjectURL(objectURL);
- };
- img.onerror = () => {
- img.style.display = 'none';
- errorFallback.style.display = 'block';
- };
- } else {
- GM_xmlhttpRequest({
- method: 'GET',
- url: imageUrl,
- responseType: 'blob',
- onload: function(response) {
- if (response.status === 200 && response.response) {
- example.blob = response.response;
- const objectURL = URL.createObjectURL(response.response);
- img.src = objectURL;
- img.onload = () => {
- errorFallback.style.display = 'none';
- img.style.display = 'block';
- URL.revokeObjectURL(objectURL);
- };
- img.onerror = () => {
- img.style.display = 'none';
- errorFallback.style.display = 'block';
- };
- } else {
- errorFallback.style.display = 'block';
- console.error('Failed to load image:', imageUrl);
- }
- },
- onerror: function() {
- errorFallback.style.display = 'block';
- console.error('GM_xmlhttpRequest error for', imageUrl);
- }
- });
- }
- return img;
- } else {
- // Non-iOS: just add the image
- 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 = 'immersion-kit-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;
- stopCurrentAudio();
- 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;
- stopCurrentAudio();
- 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 = 'immersion-kit-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
- const existingNavigationDiv = document.getElementById('immersion-kit-embed');
- if (existingNavigationDiv) existingNavigationDiv.remove();
- renderImageAndPlayAudio(state.vocab, CONFIG.AUTO_PLAY_SOUND);
- preloadImages();
- }
- function replaceSpecialCharacters(text) {
- // Replace special characters in the text
- return text.replace(/<br>/g, '\n').replace(/"/g, '"').replace(/\n/g, '<br>');
- }
- //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 deleteCurrentVocabButton = createDeleteCurrentVocabButton('400px');
- const actionButtonsContainer = document.createElement('div');
- actionButtonsContainer.style.textAlign = 'center';
- actionButtonsContainer.style.marginTop = '10px';
- actionButtonsContainer.append(closeButton, saveButton, defaultButton, deleteButton, deleteCurrentVocabButton);
- 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');
- const changes = gatherChanges(inputs);
- applyChanges(changes);
- finalizeSaveConfig();
- setVocabSize();
- setPageWidth();
- }
- function gatherChanges(inputs) {
- 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);
- changes[configPrefix + key] = value + originalFormattedType;
- }
- });
- return changes;
- }
- 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
- async function deleteCurrentVocab() {
- try {
- const db = await IndexedDBManager.open();
- let currentVocab = state.vocab;
- // Wrap currentVocab with angle quotes if exactSearch is true
- if (state.exactSearch) {
- currentVocab = `「${currentVocab}」`;
- }
- // Delete from IndexedDB
- await IndexedDBManager.deleteEntry(db, currentVocab);
- console.log('Deleting from IndexedDB:', currentVocab);
- // Delete from local storage
- const localStorageKey = scriptPrefix + state.vocab;
- if (localStorage.getItem(localStorageKey)) {
- localStorage.removeItem(localStorageKey);
- console.log('Deleting from local storage:', localStorageKey);
- }
- alert('Current vocabulary deleted successfully!');
- location.reload();
- } catch (error) {
- console.error('Error deleting current vocabulary:', error);
- alert('Error deleting current vocabulary.');
- }
- }
- function createDeleteCurrentVocabButton(width) {
- const deleteCurrentVocabButton = createButton('Refresh Current Vocab from API', '10px', deleteCurrentVocab, width);
- deleteCurrentVocabButton.style.backgroundColor = '#C82800';
- deleteCurrentVocabButton.style.color = 'white';
- return deleteCurrentVocabButton;
- }
- 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 (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 === '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;
- }
- }
- }
- //MAIN FUNCTIONS=====================================================================================================================
- function onPageLoad() {
- // Initialize state and determine vocabulary based on URL
- state.embedAboveSubsectionMeanings = false;
- const url = window.location.href;
- const machineTranslationFrame = document.getElementById('machine-translation-frame');
- // Proceed only if the machine translation frame is not present
- if (!machineTranslationFrame) {
- //display embed for first time with loading text
- embedImageAndPlayAudio();
- setPageWidth();
- if (url.includes('/vocabulary/')) {
- state.vocab = 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 = parseVocabFromReview();
- }
- } else {
- console.log('Machine translation frame detected, skipping vocabulary parsing.');
- }
- // Retrieve stored data for the current vocabulary
- const { index, exactState } = getStoredData(state.vocab);
- state.currentExampleIndex = index;
- state.exactSearch = exactState;
- // Fetch data and embed image/audio if necessary
- if (state.vocab && !state.apiDataFetched) {
- getImmersionKitData(state.vocab, state.exactSearch)
- .then(() => {
- preloadImages();
- embedImageAndPlayAudio();
- })
- .catch(console.error);
- } else if (state.apiDataFetched) {
- embedImageAndPlayAudio();
- //preloadImages();
- setVocabSize();
- setPageWidth();
- }
- }
- 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();
- })();