您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Whitelist lock screen with key verification, animated settings, and changelog
// ==UserScript== // @name Whitelist Key Lock V2 // @namespace http://tampermonkey.net/ // @version 1.2.2 // @description Whitelist lock screen with key verification, animated settings, and changelog // @match https://www.fluentu.com/spanish/learning/* // @grant GM_xmlhttpRequest // @connect drive.google.com // @connect docs.google.com // @connect googleusercontent.com // @connect * // ==/UserScript== // Global state let WHITELIST = null; let isLoadingWhitelist = false; let whitelistCallbacks = []; const FILE_ID = '1QL-7XnZWLY5iVdVOKWCxPLaqZoerYRa7'; const RAW_URL = `https://drive.google.com/uc?id=${FILE_ID}&export=download`; // Global state let overlay = null; let statusCircle = null; let isOverlayVisible = false; let autoAnswerEnabled = false; let hasValidKey = false; let keyPromptShown = false; let lastQuestionSnapshot = ''; let whitelistLoaded = false; // Add a global flag to prevent multiple simultaneous fills let isFillingDataWord = false; // --- CONFIG --- const CHANGELOG_FILE_ID = '1M5ALLc3lx1m23gOEhoHraeX0kiK8lw3W'; const CHANGELOG_URL = `https://drive.google.com/uc?id=${CHANGELOG_FILE_ID}&export=download`; const CHANGELOG_LOCAL_KEY = 'wl_last_seen_changelog_version'; const AUTO_MODE_KEY = 'wl_auto_mode'; const DARK_MODE_KEY = 'wl_dark_mode'; // SVGs for icons (inline for portability) const COG_ICON = `<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="14" cy="14" r="5" fill="#888"/><g stroke="#444" stroke-width="2" stroke-linecap="round"><path d="M14 2v3M14 23v3M4.93 4.93l2.12 2.12M20.95 20.95l2.12 2.12M2 14h3M23 14h3M4.93 23.07l2.12-2.12M20.95 7.05l2.12-2.12"/></g><circle cx="14" cy="14" r="9" stroke="#444" stroke-width="2"/></svg>`; const CHANGELOG_ICON = `<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="4" y="4" width="20" height="20" rx="5" fill="#fff" stroke="#222" stroke-width="2"/><rect x="7" y="8" width="14" height="2" rx="1" fill="#2196F3"/><rect x="7" y="12" width="10" height="2" rx="1" fill="#2196F3"/><rect x="7" y="16" width="6" height="2" rx="1" fill="#2196F3"/><rect x="7" y="20" width="2" height="2" rx="1" fill="#2196F3"/></svg>`; // 1. Stop auto-answer interval when no questions are present let autoAnswerInterval = null; // Add whitelist caching for faster load const WHITELIST_CACHE_KEY = 'wl_cached_whitelist_v2'; const WHITELIST_CACHE_TTL = 1000 * 60 * 60 * 6; // 6 hours function getCachedWhitelist() { try { const cached = JSON.parse(localStorage.getItem(WHITELIST_CACHE_KEY)); if (cached && cached.data && Date.now() - cached.time < WHITELIST_CACHE_TTL) { return cached.data; } } catch (e) {} return null; } function setCachedWhitelist(data) { localStorage.setItem(WHITELIST_CACHE_KEY, JSON.stringify({ data, time: Date.now() })); } function fetchWhitelistFromNetwork(callback) { GM_xmlhttpRequest({ method: 'GET', url: RAW_URL, headers: { 'Accept': '*/*', 'Cache-Control': 'no-cache' }, nocache: true, onload: function(response) { try { const content = response.responseText; let parsed = null; try { const whitelist = JSON.parse(content); if (Array.isArray(whitelist)) { parsed = whitelist; } } catch (e) { const arrayMatch = content.match(/\[\s*"[^"]+(?:",\s*"[^"]+)*"\s*\]/); if (arrayMatch) { parsed = JSON.parse(arrayMatch[0]); } } callback(Array.isArray(parsed) ? parsed : null); } catch (error) { callback(null); } }, onerror: function() { callback(null); } }); } function loadWhitelist(callback) { // Try cache first const cached = getCachedWhitelist(); if (cached) { callback(cached); // Also update in background fetchWhitelistFromNetwork((fresh) => { if (fresh) setCachedWhitelist(fresh); }); return; } // Otherwise, fetch from network fetchWhitelistFromNetwork((fresh) => { if (fresh) setCachedWhitelist(fresh); callback(fresh); }); } function verifyKeyWithWhitelist(key, callback) { console.log('=== VERIFYING KEY ==='); if (!key || typeof key !== 'string') { console.log('Invalid key format'); callback(false); return; } const trimmedKey = key.trim(); loadWhitelist(whitelist => { if (!whitelist || !Array.isArray(whitelist)) { console.log('Invalid key'); callback(false); return; } // Case-sensitive comparison of keys const isValid = whitelist.some(validKey => { if (typeof validKey !== 'string') return false; return validKey.trim() === trimmedKey; }); if (!isValid) { console.log('Invalid key'); } callback(isValid); }); } function checkWhitelistKey(callback) { console.log('=== CHECKING SAVED KEY ==='); const savedKey = localStorage.getItem('my_wl_key'); if (!savedKey) { console.log('No saved key found'); callback(false); return; } verifyKeyWithWhitelist(savedKey, (isValid) => { console.log('Saved key valid?', isValid); hasValidKey = isValid; callback(isValid); }); } function removeScriptFeatures() { if (statusCircle) statusCircle.remove(); if (overlay) overlay.remove(); statusCircle = null; overlay = null; autoAnswerEnabled = false; isOverlayVisible = false; } function showLockScreen() { if (!window.location.href.startsWith('https://www.fluentu.com/spanish/learning/')) return; if (keyPromptShown) return; keyPromptShown = true; removeScriptFeatures(); createLockScreenUI(); } function createLockScreenUI() { const overlay = document.createElement('div'); overlay.style = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 999999; background: rgba(0, 0, 0, 0.85); display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: Arial, sans-serif; `; overlay.innerHTML = ` <div style=" background: #fff; padding: 2rem; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); text-align: center; max-width: 90vw; width: 400px; position: relative; "> <div id="wlclose" style=" position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 20px; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 12px; background: #f0f0f0; color: #666; transition: all 0.2s; ">×</div> <div id="wlicon" style="font-size: 64px; margin-bottom: 24px;">🔒</div> <h2 style="color: #333; margin: 0 0 1rem 0; font-size: 24px;">Enter Whitelist Key</h2> <input id="wlkey" type="text" style=" width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 16px; margin-bottom: 1rem; box-sizing: border-box; outline: none; " placeholder="Enter your key here" autofocus> <div id="wlerr" style=" color: #ff4444; min-height: 20px; margin-bottom: 1rem; font-size: 14px; "></div> <button id="wlsubmit" style=" background: #4CAF50; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background 0.2s; margin-right: 10px; ">Unlock</button> <button id="wlcontinue" style=" display: none; background: #2196F3; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background 0.2s; ">Continue</button> <button id="wlskip" style=" background: #666; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background 0.2s; ">Continue Without Key</button> <div style=" margin-top: 1rem; font-size: 12px; color: #666; ">A valid whitelist key is required to use the script's features.</div> </div> `; document.body.appendChild(overlay); const keyInput = overlay.querySelector('#wlkey'); const submitBtn = overlay.querySelector('#wlsubmit'); const continueBtn = overlay.querySelector('#wlcontinue'); const skipBtn = overlay.querySelector('#wlskip'); const errorDiv = overlay.querySelector('#wlerr'); const iconDiv = overlay.querySelector('#wlicon'); const closeBtn = overlay.querySelector('#wlclose'); function validateAndSubmit() { const key = keyInput.value.trim(); if (!key) { errorDiv.textContent = 'Please enter a key'; errorDiv.style.color = '#ff4444'; return; } verifyKeyWithWhitelist(key, (isValid) => { if (isValid) { // Valid key - show success UI iconDiv.textContent = '✅'; errorDiv.textContent = 'Valid key! Click Continue to proceed.'; errorDiv.style.color = '#4CAF50'; submitBtn.style.display = 'none'; skipBtn.style.display = 'none'; continueBtn.style.display = 'inline-block'; keyInput.disabled = true; // Set up continue button continueBtn.onclick = () => { localStorage.setItem('my_wl_key', key); hasValidKey = true; overlay.remove(); keyPromptShown = false; initializeScriptFeatures(); }; } else { errorDiv.textContent = 'Invalid key. Please try again or continue without a key.'; errorDiv.style.color = '#ff4444'; keyInput.select(); } }); } submitBtn.onclick = validateAndSubmit; skipBtn.onclick = () => { hasValidKey = false; localStorage.removeItem('my_wl_key'); removeScriptFeatures(); overlay.remove(); keyPromptShown = false; }; closeBtn.onclick = () => { overlay.remove(); keyPromptShown = false; // If we have a valid key, make sure script features are initialized if (hasValidKey) { initializeScriptFeatures(); } }; closeBtn.onmouseover = () => { closeBtn.style.background = '#e0e0e0'; closeBtn.style.color = '#333'; }; closeBtn.onmouseout = () => { closeBtn.style.background = '#f0f0f0'; closeBtn.style.color = '#666'; }; keyInput.onkeypress = (e) => { if (e.key === 'Enter') validateAndSubmit(); }; } function createStatusCircle() { statusCircle = document.createElement('div'); statusCircle.style = ` position: fixed; top: 10px; left: 10px; width: 12px; height: 12px; background: #ff4040; border-radius: 50%; z-index: 9998; cursor: pointer; transition: background-color 0.3s; `; statusCircle.title = 'Click to toggle auto-answer'; statusCircle.onclick = () => { if (!hasValidKey) { showLockScreen(); return; } autoAnswerEnabled = !autoAnswerEnabled; updateOverlayWithSecretPhrase(); updateStatusCircle(); }; document.body.appendChild(statusCircle); } function createOverlay() { overlay = document.createElement('div'); overlay.style = ` position: fixed; top: 10px; left: 30px; background: rgba(0, 0, 0, 0.8); color: white; padding: 10px; border-radius: 5px; font-family: Arial, sans-serif; font-size: 14px; z-index: 9999; display: none; transition: opacity 0.3s; `; document.body.appendChild(overlay); } function updateStatusCircle() { if (!statusCircle) return; statusCircle.style.background = autoAnswerEnabled ? '#40ff40' : '#ff4040'; } function updateOverlayWithSecretPhrase() { let phrases = []; document.querySelectorAll('div[data-studied-phrase]').forEach(div => { const phrase = div.getAttribute('data-studied-phrase'); if (phrase && !phrases.includes(phrase)) phrases.push(phrase); }); document.querySelectorAll('.trans-content').forEach(div => { const phrase = div.textContent.trim(); if (phrase && !phrases.includes(phrase)) phrases.push(phrase); }); if (!overlay) createOverlay(); const autoLabel = autoAnswerEnabled ? '<span style="color:#00ff00;">ON</span>' : '<span style="color:#ff4040;">OFF</span>'; if (phrases.length === 0) { overlay.innerHTML = `<div>Secret phrase not found</div> <div style="margin-top:6px;">Auto Answer: ${autoLabel}</div>`; } else { overlay.innerHTML = `<div>${phrases[0]?.trim() || 'Secret phrase not found'}</div> <div style="margin-top:6px;">Auto Answer: ${autoLabel}</div>`; } updateStatusCircle(); } function getAnswerWord() { const word = document.querySelector('.quiz-clip-preview-active-word[data-word]')?.getAttribute('data-word')?.trim() || ''; // Clean up any ellipses and extra spaces return word.replace('...', ' ').replace(/\s+/g, ' ').trim(); } function normalizeText(text) { if (!text) return ''; return text .replace(/\.{3,}/g, ' ') // Replace ellipses with space .replace(/\s+/g, ' ') // Normalize multiple spaces .replace(/[.,!¡?¿;:()\[\]{}*_]/g, '') // Remove punctuation .trim() .toLowerCase(); } async function simulateInputEvents(element, value) { // Focus the element element.focus(); element.dispatchEvent(new Event('focus', { bubbles: true })); // Clear existing value element.value = ''; element.dispatchEvent(new Event('input', { bubbles: true })); // Simulate typing each character with proper events for (let i = 0; i < value.length; i++) { const char = value[i]; // Create events with proper properties const keyDown = new KeyboardEvent('keydown', { key: char, code: `Key${char.toUpperCase()}`, bubbles: true, cancelable: true, composed: true, keyCode: char.charCodeAt(0), which: char.charCodeAt(0) }); const keyPress = new KeyboardEvent('keypress', { key: char, code: `Key${char.toUpperCase()}`, bubbles: true, cancelable: true, composed: true, keyCode: char.charCodeAt(0), which: char.charCodeAt(0), charCode: char.charCodeAt(0) }); const keyUp = new KeyboardEvent('keyup', { key: char, code: `Key${char.toUpperCase()}`, bubbles: true, cancelable: true, composed: true, keyCode: char.charCodeAt(0), which: char.charCodeAt(0) }); // Update value and dispatch events in sequence element.dispatchEvent(keyDown); element.dispatchEvent(keyPress); // Update the value element.value = value.substring(0, i + 1); // Create and dispatch input event with proper data const inputEvent = new InputEvent('input', { bubbles: true, cancelable: true, composed: true, inputType: 'insertText', data: char }); element.dispatchEvent(inputEvent); // Dispatch keyup element.dispatchEvent(keyUp); // Small delay between characters to mimic human typing if (i < value.length - 1) { await new Promise(resolve => setTimeout(resolve, 10)); } } // Final change event element.dispatchEvent(new Event('change', { bubbles: true })); // Blur the element element.dispatchEvent(new Event('blur', { bubbles: true })); // Trigger React's synthetic events if needed if (window.React && window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED) { const nativeInputEvent = new Event('input', { bubbles: true }); element.dispatchEvent(nativeInputEvent); } } function getMCStringForClick() { let answer = document.querySelector('.quiz-clip-preview-active-word[data-word]')?.getAttribute('data-word'); if (!answer) { answer = document.querySelector('.quiz-clip-preview-active-word[data-native-phrase], .pinyin-speak-click-element[data-native-phrase]')?.getAttribute('data-native-phrase'); } if (!answer && overlay) { answer = overlay.textContent.split('\n')[0].trim(); } return normalizeText(answer || ''); } async function fillBlankFromDataWord() { if (isFillingDataWord) return; isFillingDataWord = true; try { const letterInputs = [...document.querySelectorAll('input[id^="word-input-"][class*="letter"], input[id^="word-input-"][class*="space"]')].filter(inp => !inp.disabled); if (!letterInputs.length) { isFillingDataWord = false; return; } // Check if already filled const filledInputs = letterInputs.filter(input => input.value.trim() !== ''); if (filledInputs.length === letterInputs.length) { const checkBtn = document.querySelector('button[title="Shortcut: Enter"], a[title="Shortcut: Enter"]'); if (checkBtn && !checkBtn.disabled) { checkBtn.click(); if (autoAnswerEnabled) { setTimeout(() => { const contBtn = document.querySelector('#quiz-next-round button[title="Shortcut: Enter"]'); if (contBtn) contBtn.click(); }, 100); } } isFillingDataWord = false; return; } // Get the full answer phrase let answer = ''; const activeWord = document.querySelector('.quiz-clip-preview-active-word[data-word]'); if (activeWord) { answer = activeWord.getAttribute('data-word'); } else { const studiedPhrase = document.querySelector('[data-studied-phrase]'); if (studiedPhrase) { answer = studiedPhrase.getAttribute('data-studied-phrase'); } } answer = normalizeText(answer); if (!answer) { isFillingDataWord = false; return; } // Map out which inputs are space inputs const spaceInputs = letterInputs.map(input => input.className.includes('space')); // Convert answer into array of characters, preserving spaces const chars = answer.split(''); let currentInputIndex = 0; let filledCount = 0; // Instantly fill all inputs for (let i = 0; i < chars.length && currentInputIndex < letterInputs.length; i++) { const input = letterInputs[currentInputIndex]; const char = chars[i]; if (spaceInputs[currentInputIndex] && char !== ' ') continue; if (!spaceInputs[currentInputIndex] && char === ' ') continue; input.value = char; filledCount++; currentInputIndex++; } if (filledCount > 0) { // Simulate a single key event on the last input to trigger UI update const lastInput = letterInputs[letterInputs.length - 1]; lastInput.focus(); lastInput.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true})); lastInput.dispatchEvent(new KeyboardEvent('keyup', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true})); lastInput.dispatchEvent(new InputEvent('input', {bubbles: true, cancelable: true, composed: true, inputType: 'insertText', data: ''})); lastInput.dispatchEvent(new Event('change', { bubbles: true })); lastInput.blur(); // Click check button const checkBtn = document.querySelector('button[title="Shortcut: Enter"], a[title="Shortcut: Enter"]'); if (checkBtn && !checkBtn.disabled) { checkBtn.click(); if (autoAnswerEnabled) { setTimeout(() => { const contBtn = document.querySelector('#quiz-next-round button[title="Shortcut: Enter"]'); if (contBtn) contBtn.click(); }, 100); } } } if (window.wl_auto_mode === 'fast' && !window._fastAutoAnswered) { window._fastAutoAnswered = true; localStorage.setItem('wl_fast_auto_answered', 'true'); } } finally { isFillingDataWord = false; } } async function fillBlankFromPhrase() { // Find the contenteditable input const input = document.querySelector('div[contenteditable="true"].js-quiz-answer-input-field, #translator-input'); if (!input) { console.log('No input found'); return; } // Try multiple methods to find the answer let answer = null; // Method 1: Check active word with data-word const activeWord = document.querySelector('.clip-preview-term-instance.word[data-word]'); if (activeWord) { answer = activeWord.getAttribute('data-word'); } // Method 2: Check data-studied-phrase elements if (!answer) { const studiedPhraseEl = document.querySelector('[data-studied-phrase]'); if (studiedPhraseEl) { answer = studiedPhraseEl.getAttribute('data-studied-phrase'); } } // Method 3: Check for native phrase if (!answer) { const nativePhraseEl = document.querySelector('[data-native-phrase]'); if (nativePhraseEl) { answer = nativePhraseEl.getAttribute('data-native-phrase'); } } if (!answer) { console.log('No answer found'); return; } // Normalize the answer text answer = normalizeText(answer); console.log('Found answer:', answer); // Focus and click the input first input.focus(); input.click(); // Clear existing content input.textContent = ''; // Type each character with proper events for (let i = 0; i < answer.length; i++) { const char = answer[i]; // Create keyboard events const keyDown = new KeyboardEvent('keydown', { key: char, code: `Key${char.toUpperCase()}`, bubbles: true, cancelable: true, composed: true, keyCode: char.charCodeAt(0), which: char.charCodeAt(0) }); const keyPress = new KeyboardEvent('keypress', { key: char, code: `Key${char.toUpperCase()}`, bubbles: true, cancelable: true, composed: true, keyCode: char.charCodeAt(0), which: char.charCodeAt(0), charCode: char.charCodeAt(0) }); // Dispatch keydown and keypress input.dispatchEvent(keyDown); input.dispatchEvent(keyPress); // Update content input.textContent = answer.substring(0, i + 1); // Create and dispatch input event const inputEvent = new InputEvent('input', { inputType: 'insertText', data: char, bubbles: true, cancelable: true, composed: true }); input.dispatchEvent(inputEvent); // Dispatch keyup const keyUp = new KeyboardEvent('keyup', { key: char, code: `Key${char.toUpperCase()}`, bubbles: true, cancelable: true, composed: true, keyCode: char.charCodeAt(0), which: char.charCodeAt(0) }); input.dispatchEvent(keyUp); // Slightly faster delay between characters if (i < answer.length - 1) { await new Promise(resolve => setTimeout(resolve, 5)); } } // Final change event input.dispatchEvent(new Event('change', { bubbles: true })); // Small delay before clicking check await new Promise(resolve => setTimeout(resolve, 25)); // Simulate Enter key press const enterKeyDown = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true }); input.dispatchEvent(enterKeyDown); document.dispatchEvent(enterKeyDown); // Click check button const checkBtn = document.querySelector('button[title="Shortcut: Enter"], a[title="Shortcut: Enter"]'); if (checkBtn && !checkBtn.disabled) { checkBtn.click(); } // If auto-answer is enabled, click continue after a short delay if (autoAnswerEnabled) { setTimeout(() => { const contBtn = document.querySelector('#quiz-next-round button[title="Shortcut: Enter"]'); if (contBtn) contBtn.click(); }, 100); } if (window.wl_auto_mode === 'fast' && !window._fastAutoAnswered) { window._fastAutoAnswered = true; localStorage.setItem('wl_fast_auto_answered', 'true'); } } function clickCorrectAnswer() { const finalMCString = getMCStringForClick(); if (!finalMCString) return; let clicked = false; const buttons = Array.from(document.querySelectorAll('button.btn-answer')); let idx = 0; function clickNext() { if (idx >= buttons.length) return; const button = buttons[idx]; const label = button.querySelector('label'); if (label) { const buttonText = normalizeText(label.textContent); if (buttonText.includes(finalMCString) || finalMCString.includes(buttonText)) { button.click(); clicked = true; // Set fast auto answered flag ONLY after a real answer if (window.wl_auto_mode === 'fast' && !window._fastAutoAnswered) { window._fastAutoAnswered = true; localStorage.setItem('wl_fast_auto_answered', 'true'); } } } idx++; setTimeout(clickNext, 100); // 100ms delay between each button } clickNext(); if (clicked) { setTimeout(() => { const checkBtn = document.querySelector('button[title="Shortcut: Enter"], a[title="Shortcut: Enter"]'); if (checkBtn && !checkBtn.disabled) checkBtn.click(); }, 150); } } // Improved helper for the new question type (button order) async function answerButtonOrderQuestion() { // 1. Find all visible, enabled answer buttons with a data-place attribute const buttons = Array.from(document.querySelectorAll('button[data-place]')).filter(btn => { return btn.offsetParent !== null && !btn.disabled; }); if (buttons.length === 0) { return false; } // 2. Sort buttons by their data-place value (as numbers) buttons.sort((a, b) => { return parseInt(a.getAttribute('data-place')) - parseInt(b.getAttribute('data-place')); }); // 3. Click each button in order for (let i = 0; i < buttons.length; i++) { buttons[i].click(); } // Wait for the Check button to appear and be enabled, then simulate Enter on it, or click it as fallback function tryCheckButton() { const checkBtn = document.querySelector('button[title="Shortcut: Enter"], a[title="Shortcut: Enter"]'); if (checkBtn && !checkBtn.disabled) { checkBtn.focus(); // Try to simulate Enter keydown on the button const enterEvent = new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true, composed: true }); checkBtn.dispatchEvent(enterEvent); // Fallback: also click the button directly setTimeout(() => { if (!checkBtn.disabled) checkBtn.click(); }, 50); } else { setTimeout(tryCheckButton, 100); } } tryCheckButton(); } // Helper to check if there are any actionable elements (questions or buttons) function isActionablePresent() { // Check for any question input/button if (document.querySelector('.quiz-clip-preview-active-word, div[contenteditable="true"].js-quiz-answer-input-field, #translator-input, button.btn-answer, button[data-place]')) { return true; } // Check for letter inputs const letterInputs = document.querySelectorAll('input[id^="word-input-"][class*="letter"], input[id^="word-input-"][class*="space"]'); if (letterInputs.length > 0) { // Only return true if there are unfilled inputs return Array.from(letterInputs).some(input => !input.value.trim()); } // Check for multi-word inputs const multiWordInputs = document.querySelectorAll('input[id^="word-input-"]:not([class*="letter"])'); if (multiWordInputs.length > 0) { // Only return true if there are unfilled inputs return Array.from(multiWordInputs).some(input => !input.value.trim()); } // Check for "Already Know" button if (document.querySelector('#quiz-already-know, .js-answer-tip, [title="Already Know"]')) { return true; } // Check for navigation buttons const btns = Array.from(document.querySelectorAll('button, a')); return btns.some(btn => { if (btn.disabled) return false; const txt = btn.textContent.trim().toLowerCase(); return ["continue", "next", "check", "submit"].some(word => txt === word || txt.startsWith(word + ' ')); }); } function findContinueButton() { // Try common selectors first let btn = document.querySelector('button[title="Shortcut: Enter"], a[title="Shortcut: Enter"]'); if (btn && !btn.disabled) return btn; // Fallback: search for button by text const btns = Array.from(document.querySelectorAll('button, a')); return btns.find(b => { if (b.disabled) return false; const txt = b.textContent.trim().toLowerCase(); return ["continue", "next", "check", "submit"].some(word => txt === word || txt.startsWith(word + ' ')); }) || null; } function performAnswerActions(skipContinue) { if (!hasValidKey) { showLockScreen(); return; } // Turn off auto answer if 'Percentage of video learned' or 'Accuracy' is detected if (document.body && document.body.innerText && (document.body.innerText.includes('Percentage of video learned') || document.body.innerText.includes('Accuracy'))) { autoAnswerEnabled = false; updateStatusCircle && updateStatusCircle(); updateOverlayWithSecretPhrase && updateOverlayWithSecretPhrase(); console.log('Auto answer disabled: Summary screen detected.'); return; } updateOverlayWithSecretPhrase(); // Fast mode: only answer the first question, then click Already Know for the rest if (window.wl_auto_mode === 'fast' && window._fastAutoAnswered) { // Click Already Know as fast as possible const clickAlreadyKnow = () => { const alreadyKnowBtn = document.querySelector('#quiz-already-know, .js-answer-tip, [title="Already Know"]'); if (alreadyKnowBtn) { alreadyKnowBtn.click(); } else { // If not available, try again on next animation frame requestAnimationFrame(clickAlreadyKnow); } }; clickAlreadyKnow(); return; } // PRIORITY: Check for button order question type FIRST if (document.querySelectorAll('button[data-place]').length > 0) { answerButtonOrderQuestion(); return; } // If secret phrase is not found and "Already Know" button exists, click it if (overlay && overlay.textContent.includes('Secret phrase not found')) { const alreadyKnowBtn = document.querySelector('#quiz-already-know, .js-answer-tip, [title="Already Know"]'); if (alreadyKnowBtn) { alreadyKnowBtn.click(); return; } } // Check for contenteditable input first const contentEditableInput = document.querySelector('div[contenteditable="true"].js-quiz-answer-input-field, #translator-input'); if (contentEditableInput) { fillBlankFromPhrase(); return; } // Then check for letter inputs const letterInputs = document.querySelectorAll('input[id^="word-input-"][class*="letter"]'); const multiWordInputs = document.querySelectorAll('input[id^="word-input-"]:not([class*="letter"])'); if (letterInputs.length > 0) { fillBlankFromDataWord(); return; } else if (multiWordInputs.length > 0) { fillBlankFromPhrase(); return; } // Only run MC logic if no other inputs are present if (document.querySelectorAll('button.btn-answer').length > 0) { clickCorrectAnswer(); } // Auto-progress: always click Check if autoAnswerEnabled or skipContinue === false if (autoAnswerEnabled || skipContinue === false) { const checkBtn = document.querySelector('button[title="Shortcut: Enter"], a[title="Shortcut: Enter"]'); if (checkBtn) { checkBtn.click(); } } // Only click Continue/Next if autoAnswerEnabled is true if (autoAnswerEnabled) { setTimeout(() => { const contBtn = document.querySelector('#quiz-next-round button[title="Shortcut: Enter"]'); if (contBtn) contBtn.click(); }, 100); } } function initializeScriptFeatures() { if (!hasValidKey) return; createStatusCircle(); createOverlay(); updateStatusCircle(); updateOverlayWithSecretPhrase(); setInterval(() => { if (!hasValidKey) { removeScriptFeatures(); return; } if (autoAnswerEnabled) { performAnswerActions(); } }, 300); addTopRightIcons(); } // Add settings and changelog icons to the top right function addTopRightIcons() { let iconBar = document.getElementById('wl-icon-bar'); if (!iconBar) { iconBar = document.createElement('div'); iconBar.id = 'wl-icon-bar'; iconBar.style = ` position: fixed; top: 18px; right: 24px; z-index: 100010; display: flex; gap: 12px; `; document.body.appendChild(iconBar); } // Settings (cog) let cog = document.getElementById('wl-cog-icon'); if (!cog) { cog = document.createElement('div'); cog.id = 'wl-cog-icon'; cog.innerHTML = COG_ICON; cog.style = 'width:38px;height:38px;cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:center;background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,0.08);transition:box-shadow 0.2s;'; cog.onmousedown = () => { cog.style.boxShadow = '0 1px 4px rgba(0,0,0,0.18)'; cog.style.transform = 'scale(0.96)'; }; cog.onmouseup = () => { cog.style.boxShadow = '0 2px 8px rgba(0,0,0,0.08)'; cog.style.transform = 'scale(1)'; }; cog.onclick = () => { // If changelog is open, close it and open settings if (document.querySelector('.wl-changelog-modal-roblox')) { closeChangelogModal(); setTimeout(() => showSettingsModal(), 10); } else if (!document.querySelector('.wl-settings-modal-roblox')) { showSettingsModal(); } }; iconBar.appendChild(cog); } // Changelog let changelog = document.getElementById('wl-changelog-icon'); if (!changelog) { changelog = document.createElement('div'); changelog.id = 'wl-changelog-icon'; changelog.innerHTML = CHANGELOG_ICON; changelog.style = 'width:38px;height:38px;cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:center;background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,0.08);transition:box-shadow 0.2s;'; changelog.onmousedown = () => { changelog.style.boxShadow = '0 1px 4px rgba(0,0,0,0.18)'; changelog.style.transform = 'scale(0.96)'; }; changelog.onmouseup = () => { changelog.style.boxShadow = '0 2px 8px rgba(0,0,0,0.08)'; changelog.style.transform = 'scale(1)'; }; changelog.onclick = () => { // If settings is open, close it and open changelog if (document.querySelector('.wl-settings-modal-roblox')) { document.querySelector('.wl-settings-modal-roblox')?.remove(); document.querySelector('.wl-settings-overlay-roblox')?.remove(); setTimeout(() => showChangelogModal(), 10); } else if (!document.querySelector('.wl-changelog-modal-roblox')) { showChangelogModal(); } }; iconBar.appendChild(changelog); } } // Settings modal function showSettingsModal() { // If changelog modal is open, close it and open settings after a short delay if (document.querySelector('.wl-changelog-modal-roblox')) { document.querySelector('.wl-changelog-modal-roblox')?.remove(); document.querySelector('.wl-changelog-overlay-roblox')?.remove(); setTimeout(() => showSettingsModal(), 10); return; } closeModals(); injectCartoonFont(); injectChangelogStyles(); let overlay = document.createElement('div'); overlay.className = 'wl-settings-overlay-roblox'; overlay.style = 'position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(33,150,243,0.08); z-index: 100011; cursor: pointer;'; overlay.onclick = () => { document.querySelector('.wl-settings-modal-roblox')?.remove(); overlay.remove(); }; document.body.appendChild(overlay); let modal = document.createElement('div'); modal.className = 'wl-settings-modal-roblox'; modal.innerHTML = ` <div class="wl-settings-header-roblox"><span>Settings</span><span class="wl-settings-close-roblox" id="wl-settings-close">×</span></div> <div class="wl-settings-content-roblox"> <div style="margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;"> <span style="font-size:15px;">Auto Answer Mode:</span> <select id="wl-auto-mode-select" style="font-size:15px;padding:4px 10px;border-radius:8px;border:1px solid #2196F3;background:#f7faff;color:#222;outline:none;"> <option value="auto">Normal</option> <option value="fast">Fast</option> </select> </div> <div style="font-size:12px;color:#888;margin-bottom:10px;">If it says <b>you have no words for review</b> switch from fast mode to normal mode.</div> <div style="margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;"> <span style="font-size:15px;">Dark Mode 🌓</span> <label class="wl-switch"> <input type="checkbox" id="wl-dark-mode-toggle"> <span class="wl-slider"></span> </label> </div> </div> <style> .wl-switch { position: relative; display: inline-block; width: 46px; height: 24px; } .wl-switch input { opacity: 0; width: 0; height: 0; } .wl-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; transition: .3s; border-radius: 24px; } .wl-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background: #fff; transition: .3s; border-radius: 50%; box-shadow: 0 1px 4px rgba(0,0,0,0.12); } input:checked + .wl-slider { background: #23272a; } input:checked + .wl-slider:before { transform: translateX(22px); background: #e0e0e0; } </style> `; document.body.appendChild(modal); animateIn(modal); document.getElementById('wl-settings-close').onclick = () => { animateOut(modal, () => modal.remove()); overlay.remove(); }; // Dropdown logic const modeSelect = document.getElementById('wl-auto-mode-select'); let mode = localStorage.getItem(AUTO_MODE_KEY) || 'auto'; modeSelect.value = mode; modeSelect.onchange = () => { localStorage.setItem(AUTO_MODE_KEY, modeSelect.value); window.wl_auto_mode = modeSelect.value; resetFastAutoAnswered(); }; // Dark mode toggle logic const darkToggle = document.getElementById('wl-dark-mode-toggle'); const darkPref = localStorage.getItem(DARK_MODE_KEY) === 'on'; darkToggle.checked = darkPref; darkToggle.onchange = () => { if (darkToggle.checked) { localStorage.setItem(DARK_MODE_KEY, 'on'); applyDarkMode(true); } else { localStorage.setItem(DARK_MODE_KEY, 'off'); applyDarkMode(false); } }; } // --- CHANGELOG MODAL --- function injectCartoonFont() { if (!document.getElementById('wl-cartoon-font')) { const link = document.createElement('link'); link.id = 'wl-cartoon-font'; link.rel = 'stylesheet'; link.href = 'https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap'; document.head.appendChild(link); } } function injectChangelogStyles() { if (document.getElementById('wl-changelog-style')) return; const style = document.createElement('style'); style.id = 'wl-changelog-style'; style.textContent = ` .wl-changelog-modal-roblox, .wl-settings-modal-roblox { font-family: 'Fredoka One', 'Baloo', Arial, sans-serif !important; border-radius: 24px !important; box-shadow: 0 6px 24px 0 rgba(0,0,0,0.16) !important; min-width: 220px !important; max-width: 96vw !important; width: 340px !important; background: #fff !important; border: 3px solid #2196F3 !important; overflow: visible !important; z-index: 100012 !important; position: fixed !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; padding: 0 !important; margin: 0 !important; display: flex; flex-direction: column; align-items: stretch; transition: box-shadow 0.2s; } .wl-changelog-overlay-roblox { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(33,150,243,0.08); z-index: 100011; cursor: pointer; } .wl-changelog-header-roblox, .wl-settings-header-roblox { background: linear-gradient(90deg, #2196F3 80%, #42a5f5 100%); color: #fff; font-size: 1.25rem; font-weight: 600; border-radius: 20px 20px 0 0; padding: 14px 18px 8px 18px; display: flex; align-items: center; justify-content: space-between; letter-spacing: 1px; box-shadow: 0 2px 8px rgba(33,150,243,0.08); text-shadow: 0 2px 8px rgba(33,150,243,0.18), 0 1px 0 #fff; } .wl-changelog-header-roblox span:first-child, .wl-settings-header-roblox span:first-child { text-shadow: 0 2px 8px rgba(33,150,243,0.18), 0 1px 0 #fff; } .wl-changelog-close-roblox, .wl-settings-close-roblox { width: 32px; height: 32px; border-radius: 50%; background: #fff; color: #2196F3; display: flex; align-items: center; justify-content: center; font-size: 1.3rem; font-weight: 700; cursor: pointer; transition: background 0.18s, color 0.18s, transform 0.18s; box-shadow: 0 2px 8px rgba(33,150,243,0.10); border: 2px solid #2196F3; } .wl-changelog-close-roblox:hover, .wl-settings-close-roblox:hover { background: #2196F3; color: #fff; transform: scale(1.08); } .wl-changelog-content-roblox, .wl-settings-content-roblox { padding: 18px 18px 12px 18px; max-height: 320px; overflow-y: auto; font-size: 1.02rem; background: #f7faff; border-radius: 0 0 20px 20px; } .wl-changelog-bubble { display: flex; align-items: flex-start; gap: 10px; background: #fff; border-left: 5px solid #2196F3; border-radius: 14px; box-shadow: 0 2px 8px rgba(33,150,243,0.08); margin-bottom: 10px; padding: 10px 12px 10px 12px; font-size: 0.98rem; color: #222; position: relative; animation: wl-bubble-in 0.5s cubic-bezier(.4,2,.6,1); } .wl-changelog-bubble-icon { font-size: 1.1rem; color: #2196F3; margin-top: 2px; flex-shrink: 0; } @keyframes wl-bubble-in { 0% { opacity: 0; transform: translateY(20px) scale(0.95); } 100% { opacity: 1; transform: translateY(0) scale(1); } } .wl-changelog-date-roblox { color: #2196F3; font-weight: 700; margin-bottom: 12px; font-size: 0.98rem; text-align: right; } .wl-changelog-error-roblox { color: #e74c3c; font-weight: 700; text-align: center; margin-top: 18px; font-size: 1.02rem; } .wl-loading-anim-roblox { display: flex; gap: 2px; font-family: 'Fredoka One', 'Baloo', Arial, sans-serif; font-size: 1.1rem; margin-top: 12px; justify-content: center; } .wl-loading-anim-roblox span { display: inline-block; transition: transform 0.3s cubic-bezier(.4,2,.6,1); animation: wl-bounce 1.1s infinite; } .wl-loading-anim-roblox span:nth-child(1) { animation-delay: 0s; } .wl-loading-anim-roblox span:nth-child(2) { animation-delay: 0.1s; } .wl-loading-anim-roblox span:nth-child(3) { animation-delay: 0.2s; } .wl-loading-anim-roblox span:nth-child(4) { animation-delay: 0.3s; } .wl-loading-anim-roblox span:nth-child(5) { animation-delay: 0.4s; } .wl-loading-anim-roblox span:nth-child(6) { animation-delay: 0.5s; } .wl-loading-anim-roblox span:nth-child(7) { animation-delay: 0.6s; } .wl-loading-anim-roblox span:nth-child(8) { animation-delay: 0.7s; } .wl-loading-anim-roblox span:nth-child(9) { animation-delay: 0.8s; } .wl-loading-anim-roblox span:nth-child(10) { animation-delay: 0.9s; } @keyframes wl-bounce { 0%, 100% { transform: translateY(0); } 40% { transform: translateY(-12px); } 60% { transform: translateY(-7px); } } `; document.head.appendChild(style); } let wlChangelogCache = null; function preloadChangelog() { GM_xmlhttpRequest({ method: 'GET', url: CHANGELOG_URL, headers: { 'Accept': 'application/json' }, onload: function(response) { try { wlChangelogCache = JSON.parse(response.responseText); } catch (e) { wlChangelogCache = null; } }, onerror: function() { wlChangelogCache = null; } }); } function showChangelogModal(force) { // If settings modal is open, close it and open changelog after a short delay if (document.querySelector('.wl-settings-modal-roblox')) { document.querySelector('.wl-settings-modal-roblox')?.remove(); document.querySelector('.wl-settings-overlay-roblox')?.remove(); setTimeout(() => showChangelogModal(force), 10); return; } injectCartoonFont(); injectChangelogStyles(); document.querySelector('.wl-changelog-modal-roblox')?.remove(); document.querySelector('.wl-changelog-overlay-roblox')?.remove(); let overlay = document.createElement('div'); overlay.className = 'wl-changelog-overlay-roblox'; overlay.onclick = () => closeChangelogModal(); document.body.appendChild(overlay); let modal = document.createElement('div'); modal.className = 'wl-changelog-modal-roblox'; modal.innerHTML = ` <div class="wl-changelog-header-roblox"><span>Changelog</span><span class="wl-changelog-close-roblox" id="wl-changelog-close">×</span></div> <div class="wl-changelog-content-roblox" id="wl-changelog-content"> <div class="wl-loading-anim-roblox"> <span>L</span><span>o</span><span>a</span><span>d</span><span>i</span><span>n</span><span>g</span><span>.</span><span>.</span><span>.</span> </div> </div> `; document.body.appendChild(modal); animateIn(modal); function closeHandler() { closeChangelogModal(); } document.getElementById('wl-changelog-close').onclick = closeHandler; window.addEventListener('keydown', function escHandler(e) { if (e.key === 'Escape') { closeChangelogModal(); window.removeEventListener('keydown', escHandler); } }); // Use cache if available if (wlChangelogCache) { let html = `<div class='wl-changelog-date-roblox' style='text-align:center;font-size:1.05em;margin-bottom:18px;'>Last updated: ${wlChangelogCache.date}</div>`; for (const entry of wlChangelogCache.entries) { html += `<div class='wl-changelog-bubble'><span class='wl-changelog-bubble-icon'>⭐</span><span>${entry}</span></div>`; } document.getElementById('wl-changelog-content').innerHTML = html; document.getElementById('wl-changelog-close').onclick = closeHandler; if (!force) localStorage.setItem(CHANGELOG_LOCAL_KEY, wlChangelogCache.version); } else { // Fallback: fetch if not yet loaded GM_xmlhttpRequest({ method: 'GET', url: CHANGELOG_URL, headers: { 'Accept': 'application/json' }, onload: function(response) { try { const data = JSON.parse(response.responseText); wlChangelogCache = data; let html = `<div class='wl-changelog-date-roblox' style='text-align:center;font-size:1.05em;margin-bottom:18px;'>Last updated: ${data.date}</div>`; for (const entry of data.entries) { html += `<div class='wl-changelog-bubble'><span class='wl-changelog-bubble-icon'>⭐</span><span>${entry}</span></div>`; } document.getElementById('wl-changelog-content').innerHTML = html; document.getElementById('wl-changelog-close').onclick = closeHandler; if (!force) localStorage.setItem(CHANGELOG_LOCAL_KEY, data.version); } catch (e) { document.getElementById('wl-changelog-content').innerHTML = '<div class="wl-changelog-error-roblox">Failed to load changelog.</div>'; document.getElementById('wl-changelog-close').onclick = closeHandler; } }, onerror: function() { document.getElementById('wl-changelog-content').innerHTML = '<div class="wl-changelog-error-roblox">Failed to load changelog.</div>'; document.getElementById('wl-changelog-close').onclick = closeHandler; } }); } } function closeChangelogModal() { document.querySelector('.wl-changelog-modal-roblox')?.remove(); document.querySelector('.wl-changelog-overlay-roblox')?.remove(); } // --- MAIN ENTRY --- (function() { if (!window.location.href.startsWith('https://www.fluentu.com/spanish/learning/')) return; // Run key check immediately let keyChecked = false; checkWhitelistKey((isValid) => { keyChecked = true; if (!isValid) { hasValidKey = false; removeScriptFeatures(); } else { hasValidKey = true; initializeScriptFeatures(); } }); // Defer event listeners until key is checked document.addEventListener('keydown', function(e) { if (!window.location.href.startsWith('https://www.fluentu.com/spanish/learning/')) return; if (!keyChecked) return; // Ignore until key is checked // Handle backtick key - always shows whitelist screen if (e.key === '`') { e.preventDefault(); showLockScreen(); return; } // Only show prompt for hotkeys if no valid key if (!hasValidKey && (e.key === 'Tab' || e.key === 'Control' || e.key === '\\')) { e.preventDefault(); showLockScreen(); return; } // Only handle other hotkeys if we have a valid key if (hasValidKey) { if (e.key === '\\') { e.preventDefault(); isOverlayVisible = !isOverlayVisible; if (!overlay) createOverlay(); overlay.style.display = isOverlayVisible ? 'block' : 'none'; if (isOverlayVisible) updateOverlayWithSecretPhrase(); } if (e.key === 'Tab') { e.preventDefault(); autoAnswerEnabled = !autoAnswerEnabled; // Reset fast mode state when toggling auto answer window._fastAutoAnswered = false; updateOverlayWithSecretPhrase(); updateStatusCircle(); } if (e.key === 'Control') { e.preventDefault(); // Enter the correct answer and click check (but do not click continue unless autoAnswerEnabled) performAnswerActions(false); } } }); // Preload the whitelist when the script starts loadWhitelist(() => {}); // Preload changelog as soon as possible preloadChangelog(); })(); // --- ANIMATION HELPERS --- function animateIn(el) { el.style.opacity = '0'; el.style.transform = 'scale(0.95)'; setTimeout(() => { el.style.transition = 'all 0.25s cubic-bezier(.4,2,.6,1)'; el.style.opacity = '1'; el.style.transform = 'scale(1)'; }, 10); } function animateOut(el, cb) { el.style.transition = 'all 0.2s cubic-bezier(.4,2,.6,1)'; el.style.opacity = '0'; el.style.transform = 'scale(0.95)'; setTimeout(() => { if (cb) cb(); }, 200); } function closeModals() { document.getElementById('wl-settings-modal')?.remove(); } // --- Ensure preferred mode is always loaded on script start --- window.addEventListener('DOMContentLoaded', () => { window.wl_auto_mode = localStorage.getItem(AUTO_MODE_KEY) || 'auto'; }); window.wl_auto_mode = localStorage.getItem(AUTO_MODE_KEY) || 'auto'; function resetFastAutoAnswered() { window._fastAutoAnswered = false; localStorage.setItem('wl_fast_auto_answered', 'false'); } // Optionally, reset fast mode state when quiz changes (e.g., on navigation) window.addEventListener('popstate', resetFastAutoAnswered); window.addEventListener('hashchange', resetFastAutoAnswered); function applyDarkMode(enabled) { let style = document.getElementById('wl-dark-mode-style'); if (enabled) { if (!style) { style = document.createElement('style'); style.id = 'wl-dark-mode-style'; style.textContent = ` body, html, #app, .main-content, .content, .container, .card, .panel, .quiz-card, .quiz-panel, .quiz-content, .quiz-question, .trans-content, .vocab-card, .vocab-panel, .vocab-content, .vocab-section, .vocab-list, .vocab-header, .vocab-footer, .vocab-modal, .vocab-overlay, .vocab-popup, .vocab-dialog, .vocab-tooltip, .vocab-dropdown, .vocab-menu, .header, .footer, .video-player, .video-container, .sidebar, .nav, .navbar, .menu, .menu-bar, .menu-list, .menu-item, .dropdown, .dropdown-menu, .dropdown-item, .modal, .popup, .dialog, .tooltip, .panel, .card, .list, .list-item, .section, .content, .main, .background, .overlay, .input, .button, .form, .quiz, .quiz-header, .quiz-footer, .quiz-main, .quiz-side, .quiz-row, .quiz-col, .quiz-box, .quiz-container, .quiz-modal, .quiz-overlay, .quiz-popup, .quiz-dialog, .quiz-tooltip, .quiz-dropdown, .quiz-menu, .quiz-list, .quiz-list-item, .quiz-list-header, .quiz-list-footer, .quiz-list-content, .quiz-list-title, .quiz-list-description, .quiz-list-meta, .quiz-list-action, .quiz-list-icon, .quiz-list-avatar, .quiz-list-badge, .quiz-list-tag, .quiz-list-label, .quiz-list-value, .quiz-list-extra, .quiz-list-divider, .quiz-list-separator, .quiz-list-group, .quiz-list-subtitle, .quiz-list-note, .quiz-list-tip, .quiz-list-warning, .quiz-list-error, .quiz-list-success, .quiz-list-info, .quiz-list-empty, .quiz-list-loading, .quiz-list-more, .quiz-list-expand, .quiz-list-collapse, .quiz-list-toggle, .quiz-list-switch, .quiz-list-slider, .quiz-list-progress, .quiz-list-step, .quiz-list-dot, .quiz-list-circle, .quiz-list-bar, .quiz-list-line, .quiz-list-track, .quiz-list-thumb, .quiz-list-rail, .quiz-list-fill, .quiz-list-mask, .quiz-list-shadow, .quiz-list-glow, .quiz-list-blur, .quiz-list-fade, .quiz-list-zoom, .quiz-list-flip, .quiz-list-spin, .quiz-list-bounce, .quiz-list-shake, .quiz-list-wobble, .quiz-list-jump, .quiz-list-swing, .quiz-list-tada, .quiz-list-rubber, .quiz-list-pulse, .quiz-list-heart, .quiz-list-star, .quiz-list-fire, .quiz-list-spark, .quiz-list-flash, .quiz-list-light, .quiz-list-dark, .quiz-list-night, .quiz-list-day, .quiz-list-sun, .quiz-list-moon, .quiz-list-cloud, .quiz-list-rain, .quiz-list-snow, .quiz-list-wind, .quiz-list-thunder, .quiz-list-lightning, .quiz-list-storm, .quiz-list-fog, .quiz-list-mist, .quiz-list-haze, .quiz-list-smoke, .quiz-list-dust, .quiz-list-sand, .quiz-list-mud, .quiz-list-dirt, .quiz-list-grass, .quiz-list-leaf, .quiz-list-flower, .quiz-list-tree, .quiz-list-plant, .quiz-list-fruit, .quiz-list-vegetable, .quiz-list-meat, .quiz-list-fish, .quiz-list-egg, .quiz-list-milk, .quiz-list-bread, .quiz-list-cake, .quiz-list-cookie, .quiz-list-candy, .quiz-list-ice, .quiz-list-cream, .quiz-list-chocolate, .quiz-list-coffee, .quiz-list-tea, .quiz-list-juice, .quiz-list-water, .quiz-list-soda, .quiz-list-beer, .quiz-list-wine, .quiz-list-cocktail, .quiz-list-drink, .quiz-list-cup, .quiz-list-glass, .quiz-list-bottle, .quiz-list-can, .quiz-list-jar, .quiz-list-box, .quiz-list-bag, .quiz-list-basket { background: #23272a !important; color: #e0e0e0 !important; border-color: #444 !important; } `; document.head.appendChild(style); } document.body.classList.add('wl-dark-mode'); } else { if (style) style.remove(); document.body.classList.remove('wl-dark-mode'); } }