您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Manages tasks with a Pomodoro timer, accessible via Ctrl+T.
当前为
// ==UserScript== // @name To-Do List + Pomodoro Timer (Ctrl+T) // @namespace http://tampermonkey.net/ // @version 1.6 // @description Manages tasks with a Pomodoro timer, accessible via Ctrl+T. // @author kq // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY = 'kq_todo_pomodoro_tasks'; let tasks = []; let showExpired = false; let timerInterval = null; let timeLeft = 0; // in seconds let currentTaskForTimer = null; // Stores the object of the task being timed let isTimerPaused = false; let selectedTaskIndexForPanel = -1; // Index of task selected in management panel // --- Audio Alarm --- let audioContext; let alarmSoundBuffer; const alarmFrequency = 440; // A4 note const alarmDuration = 0.5; // seconds function setupAudio() { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } } function playAlarm() { if (!audioContext) setupAudio(); if (!audioContext) return; // Still couldn't get context const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.type = 'sine'; // sine, square, sawtooth, triangle oscillator.frequency.setValueAtTime(alarmFrequency, audioContext.currentTime); // Hz gainNode.gain.setValueAtTime(0.5, audioContext.currentTime); // Volume oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + alarmDuration); // Vibrate for mobile (if applicable, though this is a desktop script) if (navigator.vibrate) { navigator.vibrate(200); } } // --- Data Management --- function loadTasks() { const tasksJSON = GM_getValue(STORAGE_KEY, '[]'); tasks = JSON.parse(tasksJSON); } function saveTasks() { GM_setValue(STORAGE_KEY, JSON.stringify(tasks)); } function getTaskById(id) { return tasks.find(task => task.id === id); } // --- UI Elements --- let managementPanel, taskListDiv, floatingHud, hudText, hudTime, hudProgressCircle, hudProgressBar; function createManagementPanel() { panel = document.createElement('div'); panel.id = 'todo-pomodoro-panel'; panel.innerHTML = ` <div id="panel-header"> <h2>To-Do List & Pomodoro (Ctrl+T)</h2> <button id="close-panel-btn">×</button> </div> <div id="task-input-area"> <input type="text" id="new-task-name" placeholder="Task Name"> <input type="number" id="new-task-duration" placeholder="Minutes (default 25)" min="1"> <button id="add-task-btn">Add Task</button> </div> <div id="task-filters"> <label> <input type="checkbox" id="show-expired-checkbox"> Show Expired </label> </div> <div id="task-list-container"></div> <div id="panel-timer-controls"> <h3>Timer for Selected Task</h3> <div id="selected-task-name-panel">No task selected</div> <div id="selected-task-timer-panel">00:00</div> <button id="start-task-btn" disabled>Start</button> <button id="pause-task-btn" disabled>Pause</button> <button id="stop-task-btn" disabled>Stop</button> </div> `; document.body.appendChild(panel); managementPanel = panel; taskListDiv = panel.querySelector('#task-list-container'); // Event Listeners for panel panel.querySelector('#close-panel-btn').addEventListener('click', togglePanel); panel.querySelector('#add-task-btn').addEventListener('click', handleAddTask); panel.querySelector('#new-task-name').addEventListener('keypress', (e) => { if (e.key === 'Enter') handleAddTask(); }); panel.querySelector('#show-expired-checkbox').addEventListener('change', (e) => { showExpired = e.target.checked; renderTaskList(); }); panel.querySelector('#start-task-btn').addEventListener('click', handleStartPanelTimer); panel.querySelector('#pause-task-btn').addEventListener('click', handlePausePanelTimer); panel.querySelector('#stop-task-btn').addEventListener('click', handleStopPanelTimer); } function createFloatingHUD() { hud = document.createElement('div'); hud.id = 'todo-pomodoro-hud'; hud.innerHTML = ` <div id="hud-task-info"> <span id="hud-current-task-name">No active task</span> <span id="hud-completion-percentage">0%</span> </div> <div id="hud-timer-display"> <svg id="hud-progress-svg" viewBox="0 0 36 36"> <path id="hud-progress-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" fill="none" stroke="#ddd" stroke-width="3"/> <path id="hud-progress-bar" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" fill="none" stroke="#4CAF50" stroke-width="3" stroke-dasharray="100, 100" stroke-dashoffset="100"/> </svg> <span id="hud-time-text">25:00</span> </div> `; document.body.appendChild(hud); floatingHud = hud; hudText = hud.querySelector('#hud-current-task-name'); hudTime = hud.querySelector('#hud-time-text'); hudProgressBar = hud.querySelector('#hud-progress-bar'); updateFloatingHUD(); // Initial render } function togglePanel() { if (!managementPanel) createManagementPanel(); // Create if doesn't exist managementPanel.style.display = managementPanel.style.display === 'block' ? 'none' : 'block'; if (managementPanel.style.display === 'block') { renderTaskList(); // Refresh list when opened updatePanelTimerControls(); } } // --- Task Rendering & Management --- function renderTaskList() { if (!taskListDiv) return; taskListDiv.innerHTML = ''; const filteredTasks = tasks.filter(task => showExpired || !task.Expired); if (filteredTasks.length === 0) { taskListDiv.innerHTML = '<p>No tasks yet. Add one!</p>'; return; } const ul = document.createElement('ul'); filteredTasks.forEach((task, indexInAllTasks) => { // Find the original index if filtering is applied, or use a unique ID // For simplicity, we'll operate on task objects directly via their ID // The 'index' passed to handlers will be its actual index in `tasks` array. // Let's ensure tasks have a unique ID when created. const originalIndex = tasks.findIndex(t => t.id === task.id); const li = document.createElement('li'); li.className = `task-item ${task.Done ? 'done' : ''} ${task.Expired ? 'expired' : ''}`; if (originalIndex === selectedTaskIndexForPanel) { li.classList.add('selected-for-panel'); } li.dataset.taskId = task.id; li.innerHTML = ` <span class="task-name">${task.Name} (${task.Duration} min)</span> <div class="task-actions"> <button class="complete-btn">${task.Done ? 'Undo' : 'Done'}</button> <button class="expire-btn">${task.Expired ? 'Unexpire' : 'Expire'}</button> <button class="delete-btn">Delete</button> </div> `; // Select task for panel timer li.addEventListener('click', (e) => { if (e.target.tagName !== 'BUTTON') { // Don't select if clicking a button selectedTaskIndexForPanel = originalIndex; currentTaskForTimer = null; // Stop any global timer isTimerPaused = false; timeLeft = tasks[selectedTaskIndexForPanel].Duration * 60; if(timerInterval) clearInterval(timerInterval); timerInterval = null; renderTaskList(); // Re-render to show selection updatePanelTimerControls(); updateTimerDisplay(timeLeft, managementPanel.querySelector('#selected-task-timer-panel')); updateFloatingHUD(); // Reset floating HUD if a new task is selected for panel } }); li.querySelector('.complete-btn').addEventListener('click', () => toggleDone(task.id)); li.querySelector('.expire-btn').addEventListener('click', () => toggleExpired(task.id)); li.querySelector('.delete-btn').addEventListener('click', () => deleteTask(task.id)); ul.appendChild(li); }); taskListDiv.appendChild(ul); updateCompletionPercentage(); } function handleAddTask() { const nameInput = managementPanel.querySelector('#new-task-name'); const durationInput = managementPanel.querySelector('#new-task-duration'); const name = nameInput.value.trim(); const duration = parseInt(durationInput.value) || 25; if (name === '') { alert('Task name cannot be empty!'); return; } tasks.push({ id: Date.now().toString(), // Simple unique ID Name: name, Duration: duration, // in minutes Done: false, Expired: false }); saveTasks(); renderTaskList(); nameInput.value = ''; durationInput.value = ''; updateCompletionPercentage(); } function toggleDone(taskId) { const task = getTaskById(taskId); if (task) { task.Done = !task.Done; if (task.Done && currentTaskForTimer && currentTaskForTimer.id === taskId) { // If current pomodoro task is marked done, stop the timer handleStopPanelTimer(true); // Pass true to indicate it's a completion stop } saveTasks(); renderTaskList(); updateCompletionPercentage(); } } function toggleExpired(taskId) { const task = getTaskById(taskId); if (task) { task.Expired = !task.Expired; if (task.Expired && currentTaskForTimer && currentTaskForTimer.id === taskId) { // If current pomodoro task is marked expired, stop the timer handleStopPanelTimer(true); } saveTasks(); renderTaskList(); updateCompletionPercentage(); } } function deleteTask(taskId) { if (confirm('Are you sure you want to delete this task?')) { tasks = tasks.filter(task => task.id !== taskId); if (currentTaskForTimer && currentTaskForTimer.id === taskId) { handleStopPanelTimer(true); } if (selectedTaskIndexForPanel !== -1 && tasks[selectedTaskIndexForPanel] && tasks[selectedTaskIndexForPanel].id === taskId) { selectedTaskIndexForPanel = -1; } else if (selectedTaskIndexForPanel !==-1) { // adjust selectedTaskIndexForPanel if an item before it was deleted const deletedTaskOriginalIndex = tasks.findIndex(t => t.id === taskId); // This is tricky after filter // Safer to just reset selection or find by ID again const taskFormerlySelected = tasks[selectedTaskIndexForPanel]; if (!taskFormerlySelected || taskFormerlySelected.id === taskId) selectedTaskIndexForPanel = -1; } saveTasks(); renderTaskList(); updatePanelTimerControls(); // Update panel controls as selected task might be gone updateCompletionPercentage(); } } function updateCompletionPercentage() { const nonExpiredTasks = tasks.filter(task => !task.Expired); const completedNonExpired = nonExpiredTasks.filter(task => task.Done).length; const percentage = nonExpiredTasks.length > 0 ? Math.round((completedNonExpired / nonExpiredTasks.length) * 100) : 0; if (floatingHud) { floatingHud.querySelector('#hud-completion-percentage').textContent = `${percentage}%`; } } // --- Timer Logic & Display --- function formatTime(totalSeconds) { const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } function updateTimerDisplay(seconds, element) { if (element) { element.textContent = formatTime(seconds); } } function updateFloatingHUD() { if (!floatingHud) return; const circumference = 2 * Math.PI * 15.9155; // From SVG path radius if (currentTaskForTimer && !isTimerPaused) { // Timer is active and running hudText.textContent = `Task: ${currentTaskForTimer.Name}`; hudTime.textContent = formatTime(timeLeft); const durationInSeconds = currentTaskForTimer.Duration * 60; const progress = durationInSeconds > 0 ? (durationInSeconds - timeLeft) / durationInSeconds : 0; hudProgressBar.style.strokeDasharray = `${circumference}`; hudProgressBar.style.strokeDashoffset = `${circumference * (1 - progress)}`; hudProgressBar.style.stroke = '#4CAF50'; // Green for running } else if (currentTaskForTimer && isTimerPaused) { // Timer is active but paused hudText.textContent = `Paused: ${currentTaskForTimer.Name}`; hudTime.textContent = formatTime(timeLeft); const durationInSeconds = currentTaskForTimer.Duration * 60; const progress = durationInSeconds > 0 ? (durationInSeconds - timeLeft) / durationInSeconds : 0; hudProgressBar.style.strokeDasharray = `${circumference}`; hudProgressBar.style.strokeDashoffset = `${circumference * (1 - progress)}`; hudProgressBar.style.stroke = '#FFC107'; // Amber for paused } else { // No timer active or timer stopped hudText.textContent = "No active task"; hudTime.textContent = "00:00"; // Or default like "25:00" hudProgressBar.style.strokeDasharray = `${circumference}`; hudProgressBar.style.strokeDashoffset = `${circumference}`; // Empty circle hudProgressBar.style.stroke = '#ddd'; // Default/empty color } updateCompletionPercentage(); } function timerTick() { if (isTimerPaused) return; timeLeft--; updateTimerDisplay(timeLeft, managementPanel.querySelector('#selected-task-timer-panel')); updateFloatingHUD(); if (timeLeft <= 0) { clearInterval(timerInterval); timerInterval = null; playAlarm(); alert(`Time's up for task: ${currentTaskForTimer.Name}!`); // Automatically mark as done? Or prompt? For now, just stop. const task = getTaskById(currentTaskForTimer.id); if (task && !task.Done) { // Only mark done if not already done // Optionally, mark as done: // task.Done = true; // saveTasks(); // renderTaskList(); } currentTaskForTimer = null; // Clear current task isTimerPaused = false; updatePanelTimerControls(); updateFloatingHUD(); } } function updatePanelTimerControls() { if (!managementPanel) return; const startBtn = managementPanel.querySelector('#start-task-btn'); const pauseBtn = managementPanel.querySelector('#pause-task-btn'); const stopBtn = managementPanel.querySelector('#stop-task-btn'); const taskNameDisplay = managementPanel.querySelector('#selected-task-name-panel'); const taskTimerDisplay = managementPanel.querySelector('#selected-task-timer-panel'); if (selectedTaskIndexForPanel !== -1 && tasks[selectedTaskIndexForPanel]) { const selectedTask = tasks[selectedTaskIndexForPanel]; taskNameDisplay.textContent = `Task: ${selectedTask.Name}`; if (currentTaskForTimer && currentTaskForTimer.id === selectedTask.id) { // This task is the one with active timer startBtn.disabled = true; pauseBtn.disabled = false; stopBtn.disabled = false; pauseBtn.textContent = isTimerPaused ? "Resume" : "Pause"; updateTimerDisplay(timeLeft, taskTimerDisplay); } else { // A task is selected, but no timer is running for IT specifically startBtn.disabled = false; pauseBtn.disabled = true; stopBtn.disabled = true; pauseBtn.textContent = "Pause"; updateTimerDisplay(selectedTask.Duration * 60, taskTimerDisplay); } } else { // No task selected in panel taskNameDisplay.textContent = 'No task selected'; updateTimerDisplay(0, taskTimerDisplay); startBtn.disabled = true; pauseBtn.disabled = true; stopBtn.disabled = true; } } function handleStartPanelTimer() { if (selectedTaskIndexForPanel === -1 || !tasks[selectedTaskIndexForPanel]) return; if (timerInterval) clearInterval(timerInterval); // Clear any existing global timer currentTaskForTimer = tasks[selectedTaskIndexForPanel]; timeLeft = currentTaskForTimer.Duration * 60; isTimerPaused = false; timerInterval = setInterval(timerTick, 1000); updatePanelTimerControls(); updateFloatingHUD(); } function handlePausePanelTimer() { if (!currentTaskForTimer) return; isTimerPaused = !isTimerPaused; if (isTimerPaused) { // Timer is paused, no need to clear interval, just stop tick logic } else { // Resuming: if interval was cleared, restart it. // In current tick logic, interval runs, but tick does nothing if paused. } updatePanelTimerControls(); updateFloatingHUD(); } function handleStopPanelTimer(isSilent = false) { // isSilent to prevent alert if task is completed/deleted if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } if (currentTaskForTimer && !isSilent) { // alert(`Timer for "${currentTaskForTimer.Name}" stopped.`); } // Reset timer state for the task that was active if (currentTaskForTimer && selectedTaskIndexForPanel !== -1 && tasks[selectedTaskIndexForPanel] && tasks[selectedTaskIndexForPanel].id === currentTaskForTimer.id) { timeLeft = tasks[selectedTaskIndexForPanel].Duration * 60; // Reset to its original duration } else { timeLeft = 0; } currentTaskForTimer = null; isTimerPaused = false; updatePanelTimerControls(); // Reflects that no timer is active for the selected task updateFloatingHUD(); // Clears the HUD } // --- Styles --- function addStyles() { GM_addStyle(` #todo-pomodoro-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 450px; max-height: 80vh; background-color: #f9f9f9; border: 1px solid #ccc; box-shadow: 0 4px 8px rgba(0,0,0,0.1); z-index: 99999; display: none; flex-direction: column; font-family: Arial, sans-serif; } #panel-header { display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; background-color: #eee; border-bottom: 1px solid #ccc; } #panel-header h2 { margin: 0; font-size: 1.2em; } #close-panel-btn { background: none; border: none; font-size: 1.5em; cursor: pointer; } #task-input-area { padding: 15px; display: flex; gap: 10px; border-bottom: 1px solid #eee; } #task-input-area input[type="text"] { flex-grow: 1; padding: 8px; } #task-input-area input[type="number"] { width: 120px; padding: 8px; } #task-input-area button { padding: 8px 12px; cursor: pointer; background-color: #4CAF50; color: white; border: none; } #task-filters { padding: 10px 15px; border-bottom: 1px solid #eee; } #task-list-container { padding: 10px 15px; overflow-y: auto; flex-grow: 1; /* Allows list to take available space */ } #task-list-container ul { list-style: none; padding: 0; margin: 0; } .task-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 5px; border-bottom: 1px solid #eee; cursor: default; /* Default cursor for item */ } .task-item.selected-for-panel { background-color: #e0e0e0; font-weight: bold; } .task-item:hover:not(.selected-for-panel) { background-color: #f0f0f0; } .task-item.done .task-name { text-decoration: line-through; color: #888; } .task-item.expired .task-name { color: #aaa; font-style: italic; } .task-name { flex-grow: 1; } .task-actions button { margin-left: 5px; padding: 3px 6px; cursor: pointer; font-size: 0.8em; } #panel-timer-controls { padding: 15px; border-top: 1px solid #ccc; text-align: center; } #panel-timer-controls h3 { margin-top: 0; font-size: 1em; } #selected-task-name-panel { margin-bottom: 5px; font-style: italic; } #selected-task-timer-panel { font-size: 1.8em; margin-bottom: 10px; font-weight: bold; } #panel-timer-controls button { padding: 8px 15px; margin: 0 5px; cursor: pointer; } #panel-timer-controls button:disabled { background-color: #ccc; cursor: not-allowed; } #todo-pomodoro-hud { position: fixed; bottom: 20px; right: 20px; background-color: rgba(255, 255, 255, 0.9); border: 1px solid #ccc; border-radius: 8px; padding: 10px 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); z-index: 99998; display: flex; align-items: center; gap: 15px; font-family: Arial, sans-serif; min-width: 220px; /* Ensure enough space */ } #hud-task-info { display: flex; flex-direction: column; flex-grow: 1; } #hud-current-task-name { font-size: 0.9em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px;} #hud-completion-percentage { font-size: 0.8em; color: #555; } #hud-timer-display { position: relative; width: 40px; height: 40px; } #hud-progress-svg { width: 100%; height: 100%; transform: rotate(-90deg); /* Start from top */ } #hud-progress-bg { stroke-linecap: round; } #hud-progress-bar { stroke-linecap: round; transition: stroke-dashoffset 0.3s linear, stroke 0.3s linear; } #hud-time-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 0.8em; font-weight: bold; } `); } // --- Initialization --- function init() { loadTasks(); addStyles(); createFloatingHUD(); // Create HUD first so it's always there // Management panel is created on first toggle document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key.toLowerCase() === 't') { e.preventDefault(); togglePanel(); } }); setupAudio(); } init(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址