您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Insert visible & copyable A/B/C/D... in front of each quiz option <p>, add button to copy overall explanation only when next question button exists.
// ==UserScript== // @name Udemy Quiz helper // @namespace http://tampermonkey.net/ // @version 2025-08-22 // @description Insert visible & copyable A/B/C/D... in front of each quiz option <p>, add button to copy overall explanation only when next question button exists. // @author You // @match https://www.udemy.com/course/* // @icon https://www.google.com/s2/favicons?sz=64&domain=udemy.com // @grant GM_setClipboard // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // 等待元素出現 function waitForElement(selector, callback) { const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); callback(el); } }); observer.observe(document.body, { childList: true, subtree: true }); } // ======= 複製題目與選項按鈕 ======= function createQuestionButton() { const btn = document.createElement('button'); btn.innerText = '📋 複製題目與選項'; btn.style.position = 'fixed'; btn.style.top = '80px'; btn.style.right = '20px'; btn.style.zIndex = '9999'; btn.style.backgroundColor = '#4CAF50'; btn.style.color = '#fff'; btn.style.border = 'none'; btn.style.padding = '10px 15px'; btn.style.borderRadius = '6px'; btn.style.fontSize = '14px'; btn.style.cursor = 'pointer'; btn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)'; btn.addEventListener('click', () => { const questionEl = document.querySelector('#question-prompt'); if (!questionEl) { btn.style.backgroundColor = '#f44336'; // 紅色 setTimeout(() => btn.style.backgroundColor = '#4CAF50', 800); return; } // 取得題目文字 const questionText = questionEl.querySelector('p')?.innerText.trim() || ''; // 抓取選項文字 const options = []; document.querySelectorAll('ul.ud-unstyled-list > li').forEach((li, i) => { const answerDiv = li.querySelector( 'div.ud-heading-md div[class^="mc-quiz-answer--answer-body--"][class*="rt-scaffolding"]' ); if (answerDiv) { options.push(`${String.fromCharCode(65 + i)}. ${answerDiv.innerText.trim()}`); } }); const fullText = `【題目】\n${questionText}\n\n【選項】\n${options.join('\n')}`; GM_setClipboard(fullText); const originalColor = btn.style.backgroundColor; btn.style.backgroundColor = '#FFD700'; // 黃色 setTimeout(() => { btn.style.backgroundColor = originalColor; }, 800); }); document.body.appendChild(btn); } waitForElement('#question-prompt', createQuestionButton); // ======= 新增「複製總體解釋」按鈕 ======= function createExplanationButton() { // 如果不存在下一題按鈕,不顯示 const nextBtn = document.querySelector('button[data-purpose="go-to-next-question"]'); if (!nextBtn) return; // 避免重複插入 if (document.querySelector('.copy-overall-explanation-btn')) return; const btn = document.createElement('button'); btn.innerText = '📄 複製總體解釋'; btn.className = 'copy-overall-explanation-btn'; btn.style.position = 'fixed'; btn.style.top = '130px'; btn.style.right = '20px'; btn.style.zIndex = '9999'; btn.style.backgroundColor = '#2196F3'; btn.style.color = '#fff'; btn.style.border = 'none'; btn.style.padding = '10px 15px'; btn.style.borderRadius = '6px'; btn.style.fontSize = '14px'; btn.style.cursor = 'pointer'; btn.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)'; btn.addEventListener('click', () => { const explanationEl = document.querySelector('#overall-explanation'); if (!explanationEl) { btn.style.backgroundColor = '#f44336'; // 紅色 setTimeout(() => btn.style.backgroundColor = '#2196F3', 800); return; } const text = explanationEl.innerText.trim(); GM_setClipboard(text); const originalColor = btn.style.backgroundColor; btn.style.backgroundColor = '#4CAF50'; // 綠色 btn.innerText = '✅ 已複製'; setTimeout(() => { btn.style.backgroundColor = originalColor; btn.innerText = '📄 複製總體解釋'; }, 1200); }); document.body.appendChild(btn); } // 動態偵測下一題按鈕是否出現 const observer = new MutationObserver(() => { const existingBtn = document.querySelector('.copy-overall-explanation-btn'); const nextBtn = document.querySelector('button[data-purpose="go-to-next-question"]'); // 如果下一題按鈕存在,且還沒有插入,就新增 if (nextBtn && !existingBtn) { createExplanationButton(); } // 如果下一題按鈕消失,就移除複製按鈕 if (!nextBtn && existingBtn) { existingBtn.remove(); } }); observer.observe(document.body, { childList: true, subtree: true }); // ======= 選項 A/B/C/D 標示 ======= function getOptionItems(root = document) { return Array.from(root.querySelectorAll('li[class^="mc-quiz-question--answer"]')); } function annotateOptions() { const items = getOptionItems(); if (!items.length) return; items.forEach((item, idx) => { const firstP = item.querySelector('p'); if (!firstP) return; const existed = firstP.querySelector('span.tm-option-letter'); if (existed) existed.remove(); const letter = indexToLetters(idx); const span = document.createElement('span'); span.className = 'tm-option-letter'; span.textContent = "選項 " + letter + '. '; span.style.fontWeight = 'bold'; span.style.marginRight = '6px'; span.style.color = '#8ab4ff'; firstP.insertBefore(span, firstP.firstChild); }); } function indexToLetters(index) { let n = index; let s = ''; do { s = String.fromCharCode(65 + (n % 26)) + s; n = Math.floor(n / 26) - 1; } while (n >= 0); return s; } let scheduled = false; function scheduleAnnotate() { if (scheduled) return; scheduled = true; requestAnimationFrame(() => { scheduled = false; annotateOptions(); }); } function setupObserver() { const root = document.body; if (!root) return; const mo = new MutationObserver((mutations) => { if (mutations.some(m => { return (m.addedNodes && [...m.addedNodes].some(n => isQuizNode(n))) || (m.removedNodes && m.removedNodes.length > 0); })) { scheduleAnnotate(); } }); mo.observe(root, { childList: true, subtree: true }); annotateOptions(); hookHistory(scheduleAnnotate); window.addEventListener('popstate', scheduleAnnotate); window.addEventListener('hashchange', scheduleAnnotate); } function isQuizNode(node) { if (!(node instanceof Element)) return false; return node.matches('li[class^="mc-quiz-question--answer"], ul[aria-labelledby="question-prompt"], [class^="question-page"], [class^="quiz-page-layout"]'); } function hookHistory(onChange) { const wrap = (type) => { const orig = history[type]; if (typeof orig !== 'function') return; history[type] = function () { const ret = orig.apply(this, arguments); try { onChange(); } catch {} return ret; }; }; wrap('pushState'); wrap('replaceState'); } setupObserver(); setInterval(annotateOptions, 3000); })();