您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Configurable countdown timer for focusing purposes.
// ==UserScript== // @name Coursera Module Countdown Timer // @namespace http://tampermonkey.net/ // @version 1.0 // @description Configurable countdown timer for focusing purposes. // @author Setnour6 // @match https://www.coursera.org/learn/*/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @license Apache-2.0 // ==/UserScript== (function() { 'use strict'; const defaultConfig = { position: 'top', backgroundColor: '#ffffff', textColor: '#2a2a2a', fontSize: '16px', padding: '6px 14px', borderRadius: '15px', boxShadow: '0 2px 10px rgba(0,0,0,0.2)', snapThreshold: 50, snapEnabled: true, lockPosition: false, debug: false, showSettings: false }; let config = Object.assign({}, defaultConfig, GM_getValue('config', {})); GM_addStyle(` .countdown-timer { position: fixed; cursor: move; user-select: none; touch-action: none; transition: all 0.3s ease; background: ${config.backgroundColor}; color: ${config.textColor}; font-size: ${config.fontSize}; padding: ${config.padding}; border-radius: ${config.borderRadius}; box-shadow: ${config.boxShadow}; z-index: 999999; font-family: Arial, sans-serif; display: flex; align-items: center; gap: 15px; } .timer-content { min-width: 80px; text-align: center; min-font-size: 10px; } .settings-icon { cursor: pointer; padding: 5px; display: flex; align-items: center; transition: transform 0.2s; } .settings-icon:hover { transform: rotate(90deg); } .settings-panel { position: absolute; background: white; padding: 12px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 1000000; width: 250px; display: none; } .settings-panel.visible { display: block; } .settings-row { margin: 10px 0; } .settings-label { display: block; margin-bottom: 5px; font-size: 14px; color: #444; } select, input[type="number"], input[type="text"] { width: 100%; padding: 5px; margin-bottom: 10px; } button { padding: 8px 15px; margin-right: 5px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 4px; } #resetDefaults { background: #dc3545 !important; margin-top: 15px; } #snapThreshold:disabled { background: #eee; cursor: not-allowed; } details { margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; } summary { cursor: pointer; font-weight: bold; margin-bottom: 10px; color: #333; user-select: none; } details[open] summary { margin-bottom: 15px; } input[type="color"] { height: 30px; width: 100%; padding: 3px; } `); const timer = document.createElement('div'); timer.className = 'countdown-timer'; timer.innerHTML = ` <div class="timer-content"></div> <div class="settings-icon">⚙️</div> <div class="settings-panel"> <details> <summary>Display Settings</summary> <div class="settings-row"> <label> <input type="checkbox" id="lockPosition" ${config.lockPosition ? 'checked' : ''}> Keep Current Position </label> </div> <div class="settings-row"> <label class="settings-label">Position:</label> <select id="position"> <option value="top">Top</option> <option value="top-right">Top Right</option> <option value="top-left">Top Left</option> <option value="bottom">Bottom</option> <option value="bottom-right">Bottom Right</option> <option value="bottom-left">Bottom Left</option> <option value="left">Left</option> <option value="right">Right</option> </select> </div> <div class="settings-row"> <label> <input type="checkbox" id="snapEnabled" ${config.snapEnabled ? 'checked' : ''}> Enable Snapping </label> </div> <div class="settings-row"> <label class="settings-label">Snap Threshold (px):</label> <input type="number" id="snapThreshold" value="${config.snapThreshold}" ${config.snapEnabled ? '' : 'disabled'}> </div> </details> <details> <summary>Appearance Settings</summary> <div class="settings-row"> <label class="settings-label">Font Size (px):</label> <input type="number" id="fontSize" value="${parseInt(config.fontSize)}" min="10"> </div> <div class="settings-row"> <label class="settings-label">Background Color:</label> <input type="color" id="backgroundColor" value="${config.backgroundColor}"> </div> <div class="settings-row"> <label class="settings-label">Text Color:</label> <input type="color" id="textColor" value="${config.textColor}"> </div> <div class="settings-row"> <label class="settings-label">Padding:</label> <input type="text" id="padding" value="${config.padding}" placeholder="e.g., 6px 14px"> </div> <div class="settings-row"> <label class="settings-label">Border Radius:</label> <input type="text" id="borderRadius" value="${config.borderRadius}" placeholder="e.g., 15px"> </div> <div class="settings-row"> <label class="settings-label">Box Shadow:</label> <input type="text" id="boxShadow" value="${config.boxShadow}" placeholder="e.g., 0 2px 10px rgba(0,0,0,0.2)"> </div> </details> <div class="settings-row"> <label class="settings-label">Debug Mode:</label> <input type="checkbox" id="debug" ${config.debug ? 'checked' : ''}> </div> <div class="settings-row"> <button id="saveSettings">Save</button> <button id="closeSettings">Close</button> </div> <div class="settings-row"> <button id="resetDefaults">Reset to Defaults</button> </div> </div> `; document.body.appendChild(timer); const timerContent = timer.querySelector('.timer-content'); const settingsPanel = timer.querySelector('.settings-panel'); const settingsIcon = timer.querySelector('.settings-icon'); const snapEnabled = timer.querySelector('#snapEnabled'); const snapThreshold = timer.querySelector('#snapThreshold'); snapEnabled.addEventListener('change', () => { snapThreshold.disabled = !snapEnabled.checked; }); let intervalId = null, currentModuleId = null, currentInitialTime = null; let isDragging = false, dragStartX = 0, dragStartY = 0, initialLeft = 0, initialTop = 0; const observer = new MutationObserver(() => { if (!config.showSettings) { checkModuleChange(); findAndProcessTime(); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true }); settingsIcon.addEventListener('click', (e) => { e.stopPropagation(); adjustPanelPosition(); settingsPanel.classList.toggle('visible'); }); timer.querySelector('#resetDefaults').addEventListener('click', () => { config = Object.assign({}, defaultConfig); GM_setValue('config', config); updateFormValues(); updateTimerStyles(); updateTimerPosition(); settingsPanel.classList.add('visible'); }); function updateFormValues() { timer.querySelector('#position').value = config.position; timer.querySelector('#snapEnabled').checked = config.snapEnabled; timer.querySelector('#snapThreshold').value = config.snapThreshold; timer.querySelector('#snapThreshold').disabled = !config.snapEnabled; timer.querySelector('#fontSize').value = parseInt(config.fontSize); timer.querySelector('#backgroundColor').value = config.backgroundColor; timer.querySelector('#textColor').value = config.textColor; timer.querySelector('#padding').value = config.padding; timer.querySelector('#borderRadius').value = config.borderRadius; timer.querySelector('#boxShadow').value = config.boxShadow; timer.querySelector('#debug').checked = config.debug; timer.querySelector('#lockPosition').checked = config.lockPosition; } function adjustPanelPosition() { const timerRect = timer.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; settingsPanel.style.left = ''; settingsPanel.style.right = ''; settingsPanel.style.top = ''; settingsPanel.style.bottom = ''; settingsPanel.style.transform = ''; if (timerRect.left > viewportWidth / 2) { settingsPanel.style.right = '100%'; settingsPanel.style.left = 'auto'; } else { settingsPanel.style.left = '100%'; settingsPanel.style.right = 'auto'; } if (timerRect.bottom > viewportHeight - 200) { settingsPanel.style.bottom = '100%'; settingsPanel.style.top = 'auto'; } else { settingsPanel.style.top = '0'; settingsPanel.style.bottom = 'auto'; } if (timerRect.top > viewportHeight * 0.7) { // If in bottom 30% of screen settingsPanel.style.bottom = '100%'; settingsPanel.style.top = 'auto'; } else { settingsPanel.style.top = '0'; settingsPanel.style.bottom = 'auto'; } } timer.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', handleDrag); document.addEventListener('mouseup', endDrag); document.addEventListener('click', (e) => { if (!timer.contains(e.target)) { settingsPanel.classList.remove('visible'); } }); timer.querySelector('#saveSettings').addEventListener('click', saveSettings); timer.querySelector('#closeSettings').addEventListener('click', () => { settingsPanel.classList.remove('visible'); }); function updateTimerPosition() { timer.style.left = ''; timer.style.right = ''; timer.style.top = ''; timer.style.bottom = ''; switch(config.position) { case 'top': timer.style.top = '10px'; timer.style.left = '50%'; timer.style.transform = 'translateX(-50%)'; break; case 'bottom': timer.style.bottom = '10px'; timer.style.left = '50%'; timer.style.transform = 'translateX(-50%)'; break; case 'left': timer.style.left = '10px'; timer.style.top = '50%'; timer.style.transform = 'translateY(-50%)'; break; case 'right': timer.style.right = '10px'; timer.style.top = '50%'; timer.style.transform = 'translateY(-50%)'; break; case 'top-right': timer.style.top = '10px'; timer.style.right = '10px'; break; case 'top-left': timer.style.top = '10px'; timer.style.left = '10px'; break; case 'bottom-right': timer.style.bottom = '10px'; timer.style.right = '10px'; break; case 'bottom-left': timer.style.bottom = '10px'; timer.style.left = '10px'; break; } } function saveSettings() { const newConfig = { lockPosition: timer.querySelector('#lockPosition').checked, position: timer.querySelector('#position').value, snapThreshold: parseInt(timer.querySelector('#snapThreshold').value), snapEnabled: timer.querySelector('#snapEnabled').checked, fontSize: Math.max(10, parseInt(timer.querySelector('#fontSize').value)) + 'px', backgroundColor: timer.querySelector('#backgroundColor').value, textColor: timer.querySelector('#textColor').value, padding: timer.querySelector('#padding').value, borderRadius: timer.querySelector('#borderRadius').value, boxShadow: timer.querySelector('#boxShadow').value, debug: timer.querySelector('#debug').checked, showSettings: false }; config = Object.assign({}, config, newConfig); GM_setValue('config', config); updateTimerStyles(); if (!config.lockPosition) { updateTimerPosition(); } settingsPanel.classList.remove('visible'); } function updateTimerStyles() { timer.style.backgroundColor = config.backgroundColor; timer.style.color = config.textColor; timer.style.fontSize = config.fontSize; timer.style.padding = config.padding; timer.style.borderRadius = config.borderRadius; timer.style.boxShadow = config.boxShadow; } function startDrag(e) { if (e.target.closest('.settings-icon') || e.target.closest('.settings-panel')) return; isDragging = true; timer.style.transition = 'none'; timer.style.transform = 'none'; const rect = timer.getBoundingClientRect(); dragStartX = e.clientX - rect.left; dragStartY = e.clientY - rect.top; initialLeft = rect.left; initialTop = rect.top; } function handleDrag(e) { if (!isDragging) return; const newX = e.clientX - dragStartX; const newY = e.clientY - dragStartY; const maxX = Math.max(window.innerWidth - timer.offsetWidth, 0); const maxY = Math.max(window.innerHeight - timer.offsetHeight, 0); const constrainedX = Math.min(Math.max(newX, 0), maxX); const constrainedY = Math.min(Math.max(newY, 0), maxY); timer.style.left = `${constrainedX}px`; timer.style.top = `${constrainedY}px`; timer.style.right = 'auto'; timer.style.bottom = 'auto'; } function endDrag() { if (!isDragging) return; isDragging = false; timer.style.transition = 'all 0.3s ease'; const finalRect = timer.getBoundingClientRect(); const moved = (finalRect.left !== initialLeft || finalRect.top !== initialTop); if (!moved || !config.snapEnabled) return; const snapPoints = { left: finalRect.left, right: window.innerWidth - finalRect.right, top: finalRect.top, bottom: window.innerHeight - finalRect.bottom }; let newX = finalRect.left; let newY = finalRect.top; if (Math.min(snapPoints.left, snapPoints.right) < config.snapThreshold) { newX = snapPoints.left < snapPoints.right ? 10 : window.innerWidth - timer.offsetWidth - 10; } if (Math.min(snapPoints.top, snapPoints.bottom) < config.snapThreshold) { newY = snapPoints.top < snapPoints.bottom ? 10 : window.innerHeight - timer.offsetHeight - 10; } const maxX = Math.max(window.innerWidth - timer.offsetWidth, 0); const maxY = Math.max(window.innerHeight - timer.offsetHeight, 0); timer.style.left = `${Math.min(Math.max(newX, 0), maxX)}px`; timer.style.top = `${Math.min(Math.max(newY, 0), maxY)}px`; } function checkModuleChange() { const selectedLink = document.querySelector('a[aria-label^="selected link"]'); if (!selectedLink) return; const newModuleId = selectedLink.href.split('/').pop(); if (newModuleId !== currentModuleId) { currentModuleId = newModuleId; clearInterval(intervalId); timerContent.textContent = 'Loading...'; } } function findAndProcessTime() { const timeContainer = document.querySelector('a[aria-label^="selected link"] .rc-EffortText'); if (!timeContainer) return; const timeElement = timeContainer.querySelector('[aria-hidden="true"]') || timeContainer.querySelector('.rc-A11yScreenReaderOnly'); if (!timeElement) return; const timeMatch = timeElement.textContent.match(/(\d+)\s*(min|minutes|m)/i); if (!timeMatch) return; const minutes = parseInt(timeMatch[1], 10); if (isNaN(minutes)) return; const newDuration = minutes * 60; if (newDuration !== currentInitialTime) { startCountdown(newDuration); } } function startCountdown(totalSeconds) { clearInterval(intervalId); currentInitialTime = totalSeconds; let remaining = totalSeconds; function update() { if (remaining >= 0) { const m = Math.floor(remaining / 60); const s = remaining % 60; timerContent.textContent = `${m}:${s.toString().padStart(2, '0')}`; remaining--; } else { timerContent.textContent = 'Time\'s up!'; timerContent.style.color = '#ff0000'; clearInterval(intervalId); } } timerContent.style.color = config.textColor; update(); intervalId = setInterval(update, 1000); } timer.querySelector('#position').value = config.position; updateTimerPosition(); updateTimerStyles(); checkModuleChange(); findAndProcessTime(); })();