连续番茄时钟和长休息
- 去支持Tempermonkey的浏览器的Add-ons安装
- 代码
- https://pomodoro.pomodorotechnique.com/ 打开后刷新一次
// ==UserScript==
// @name Automated Pomodoro with Sequence Input
// @namespace http://tampermonkey.net/
// @version 3.2
// @description Automated Pomodoro with customizable sequence input (e.g., 05+🍅+05+🍅+15+🍅+30) animation bar
// @author Syl
// @run-at document-end
// @match https://pomodoro.pomodorotechnique.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
let isRunning = false;
let timeoutHandles = [];
const style = document.createElement('style');
style.textContent = `
[data-tooltip] {
position: relative;
}
[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 4px 8px;
background: rgba(0,0,0,0.8);
color: white;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 1000;
}
@keyframes progress-animation {
from {
background-position: 0 0;
}
to {
background-position: 20px 0;
}
}
`;
document.head.appendChild(style);
// 1 函数
// 1.1 获得按钮
function findButtonByText(text) {
const buttons = Array.from(document.querySelectorAll('button'));
return buttons.find(button => button.textContent.trim() === text);
}
// 1.2 模拟点击
function simulateUserClick(element) {
if (!element) return console.error("Element not found.");
const mouseDownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window });
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
element.dispatchEvent(mouseDownEvent);
element.dispatchEvent(clickEvent);
}
// 1.3 等待刷新页面后获得按钮
function waitForButton(text, callback) {
const interval = setInterval(() => {
if (!isRunning) {
clearInterval(interval);
return;
}
const button = findButtonByText(text);
if (button) {
clearInterval(interval);
callback(button);
}
}, 100); // 将500ms改为100ms
}
// 1.4 等待页面的状态变化
function waitForPageState(targetText, timeout = 10000) {
return new Promise((resolve, reject) => {
const interval = 1000; // 轮询间隔
let elapsedTime = 0;
const checkState = setInterval(() => {
elapsedTime += interval;
if (elapsedTime > timeout) {
clearInterval(checkState);
console.error(`Timeout: Page did not reach the expected state with text "${targetText}".`);
resolve(null);
return;
}
const targetElement = Array.from(document.querySelectorAll('button, div, span'))
.find(el => el.textContent.trim().includes(targetText)); // 改为包含匹配
if (targetElement) {
console.log(`Found target text: "${targetText}" in element:`, targetElement);
clearInterval(checkState);
resolve(targetElement);
} else {
console.log(`Waiting for target text: "${targetText}"...`);
}
}, interval);
});
}
// 1.5 停止番茄时间
function stopPomodoro() {
console.log("Stopping Pomodoro...");
isRunning = false;
timeoutHandles.forEach(handle => clearTimeout(handle));
timeoutHandles = [];
statusLabel.textContent = "Let's 🍅!";
// 清空进度条容器
progressContainer.innerHTML = '';
}
// 1.6 解析序列函数 兼容不以🍅开始,但是以数字开始
function parseSequence(sequence) {
// 定义允许的休息时间
const validBreakTimes = ['02', '05', '15', '30'];
// 使用正则表达式找到第一个🍅或数字的位置
const firstStepMatch = sequence.match(/🍅|\d+/);
if (!firstStepMatch) return []; // 如果没有找到任何有效步骤,返回空数组
const firstStepIndex = firstStepMatch.index;
// 从第一个有效步骤开始截取字符串
sequence = sequence.substring(firstStepIndex);
const steps = sequence.split('+').map(step => step.trim());
// 验证并转换步骤
const validatedSteps = steps.map(step => {
if (step.includes('🍅')) {
return {
type: 'work',
duration: 25
};
} else {
// 确保休息时间是有效的
const breakTime = step.padStart(2, '0');
if (!validBreakTimes.includes(breakTime)) {
throw new Error(`Invalid break time: ${step}. Allowed break times are: ${validBreakTimes.join(', ')}`);
}
return {
type: 'break',
duration: parseInt(breakTime)
};
}
});
return validatedSteps;
}
// 1.7 开始番茄循环
function startPomodoroCycles() {
const sequence = sequenceInput.value;
const steps = parseSequence(sequence);
let currentStep = 0;
// 计算总番茄数
const totalPomodoros = steps.filter(step => step.type === 'work').length;
// 用于追踪当前是第几个番茄
let currentPomodoro = 1;
// 初始化进度条
initializeProgress(sequence);
function runStep() {
if (!isRunning || currentStep >= steps.length) {
if (currentStep >= steps.length) {
console.log("All steps completed!");
isRunning = false;
statusLabel.textContent = `🎆🤩👏🎊`;
toggleButtons(false); // 重新启用按钮
}
return;
}
// 更新进度条状态
updateProgressStatus(currentStep);
const step = steps[currentStep];
console.log(`Starting step ${currentStep + 1}:`, step);
if (step.type === 'work') {
statusLabel.textContent = `🍅 ${currentPomodoro}/${totalPomodoros}`;
waitForButton("Wind Up", (windUpButton) => {
simulateUserClick(windUpButton);
const workTimeout = setTimeout(() => {
if (!isRunning) return;
currentStep++;
// 在完成后增加计数
currentPomodoro++;
runStep();
}, step.duration * 60 * 1000);
timeoutHandles.push(workTimeout);
});
} else {
const breakDuration = step.duration.toString().padStart(2, '0');
waitForButton(breakDuration, (breakButton) => {
simulateUserClick(breakButton);
const breakTimeout = setTimeout(() => {
if (!isRunning) return;
currentStep++;
waitForPageState("Ready to Pomodoro!", 10000).then(() => {
console.log("Break completed, starting next step...");
runStep();
});
}, step.duration * 60 * 1000);
timeoutHandles.push(breakTimeout);
});
}
}
runStep();
}
// 2 UI 实现
// 2.1 创建主体
const inputContainer = document.createElement('div');
inputContainer.style.position = 'relative'; // 相对定位,不固定在页面顶层
inputContainer.style.backgroundColor = 'white';
inputContainer.style.border = '2px solid grey';
inputContainer.style.borderRadius = '10px'; // 添加圆角样式
inputContainer.style.padding = '10px';
inputContainer.style.zIndex = '1000';
inputContainer.style.flexDirection = 'column';
inputContainer.style.gap = '10px';
inputContainer.style.boxShadow = '0px 4px 6px rgba(0, 0, 0, 0.1)'; // 添加阴影
// 2.2 创建状态行
const statusRow = document.createElement('div');
statusRow.style.display = 'flex';
statusRow.style.justifyContent = 'center';
statusRow.style.marginTop = '10px';
const statusLabel = document.createElement('span');
statusLabel.textContent = "Let's 🍅!";
statusRow.appendChild(statusLabel);
statusLabel.style.fontSize = '16px'; // 设置字体大小
statusLabel.style.color = 'rgb(35, 39, 43)'; // 设置字体颜色
statusLabel.style.fontWeight = 'bold'; // 可选:设置加粗
statusLabel.style.height = '16px';
// 2.3 创建进度条容器
const progressContainer = document.createElement('div');
progressContainer.style.display = 'flex';
progressContainer.style.gap = '2px';
progressContainer.style.margin = '10px 0';
progressContainer.style.width = '230px'; // 与textarea同宽
progressContainer.style.margin = '0 auto';
progressContainer.style.padding = '10px';
// 2.4 创建序列输入行
const sequenceRow = document.createElement('div');
sequenceRow.style.display = 'flex';
sequenceRow.style.alignItems = 'center';
sequenceRow.style.justifyContent = 'space-between';
sequenceRow.style.marginBottom = '10px';
//const sequenceLabel = document.createElement('label');
//sequenceLabel.textContent = 'Sequence: ';
const sequenceInput = document.createElement('textarea');
sequenceInput.value = '>>>>🍅+05+🍅+05+🍅+05+🍅+15';
sequenceInput.placeholder = '🍅+05+🍅+15...';
sequenceInput.style.width = '230px';
sequenceInput.style.height = '100px'; // 增加高度
sequenceInput.style.fontFamily = "'Consolas','Courier New', Courier, 'Lucida Console', Monaco, 'Consolas', 'Liberation Mono', 'Menlo', monospace";
sequenceInput.style.fontSize = '14px'; // 设置合适的字体大小
sequenceInput.style.padding = '5px'; // 添加内边距使文本不会贴边
sequenceInput.style.resize = 'none'; // 禁用调整大小
//sequenceInput.style.overflow = 'hidden'; // 禁止滚动
sequenceInput.style.verticalAlign = 'top'; // 文本靠上对齐
//sequenceInput.style.wordBreak = 'break-all';
//sequenceInput.style.whiteSpace = 'pre-wrap'; // 保留空格和换行
sequenceInput.style.display = 'block';
sequenceInput.style.margin = '0 auto'; // 水平居中
// sequenceRow.appendChild(sequenceLabel);
sequenceRow.appendChild(sequenceInput);
// 2.5 创建示例提示
const hintText = document.createElement('div');
hintText.textContent = '🍅=25min work, 05/15/30=break minutes';
hintText.style.fontSize = '12px';
hintText.style.color = 'gray';
hintText.style.marginBottom = '10px';
// 2.6 创建按钮行
const buttonRow = document.createElement('div');
buttonRow.style.display = 'flex';
buttonRow.style.justifyContent = 'space-between'; // 两个按钮保持间距
buttonRow.style.alignItems = 'center';
buttonRow.style.width = '200px'; // 总宽度为200px 给四个按钮
buttonRow.style.margin = '0 auto'; // 居中显示
const addPomodoroButton = document.createElement('button');
addPomodoroButton.textContent = '🍅';
addPomodoroButton.style.width = '30px';
addPomodoroButton.style.height = '30px';
addPomodoroButton.setAttribute('data-tooltip', 'insert 🍅+05');
addPomodoroButton.style.alignItems = 'center';
addPomodoroButton.style.display = 'flex';
addPomodoroButton.style.justifyContent = 'center';
addPomodoroButton.style.borderRadius = '20%';
const addPizzaButton = document.createElement('button');
addPizzaButton.textContent = '🍕';
addPizzaButton.style.width = '30px';
addPizzaButton.style.height = '30px';
addPizzaButton.setAttribute('data-tooltip', 'insert 4x(🍅+05)');
addPizzaButton.style.alignItems = 'center';
addPizzaButton.style.display = 'flex';
addPizzaButton.style.justifyContent = 'center';
addPizzaButton.style.borderRadius = '20%';
const startButton = document.createElement('button');
startButton.textContent = '▶️';
startButton.style.width = '30px'; // 每个按钮宽度
startButton.style.height = '30px'; // 按钮高度
startButton.style.alignItems = 'center';
startButton.style.display = 'flex';
startButton.style.justifyContent = 'center';
startButton.style.borderRadius = '20%';
const endButton = document.createElement('button');
endButton.textContent = '⏹️';
endButton.style.width = '30px'; // 每个按钮宽度
endButton.style.height = '30px'; // 按钮高度
endButton.style.alignItems = 'center';
endButton.style.display = 'flex';
endButton.style.justifyContent = 'center';
endButton.style.borderRadius = '20%';
// 添加“缩小”按钮
const minimizeButton = document.createElement('button');
minimizeButton.textContent = '-';
minimizeButton.style.position = 'absolute';
minimizeButton.style.top = '5px';
minimizeButton.style.right = '5px';
minimizeButton.style.width = '20px';
minimizeButton.style.height = '20px';
minimizeButton.style.lineHeight = '15px';
minimizeButton.style.textAlign = 'center';
minimizeButton.style.padding = '0';
minimizeButton.style.borderRadius = '50%'; // 圆形按钮
buttonRow.appendChild(addPomodoroButton);
buttonRow.appendChild(addPizzaButton);
buttonRow.appendChild(startButton);
buttonRow.appendChild(endButton);
// 将新元素添加到容器
inputContainer.appendChild(minimizeButton);
inputContainer.appendChild(statusRow);
inputContainer.appendChild(progressContainer);
inputContainer.appendChild(sequenceRow);
inputContainer.appendChild(hintText);
inputContainer.appendChild(buttonRow);
// 插入指定位置
let inputInserted = false;
const targetSelector = "div.bx-task-active"; // 目标选择器
const observer = new MutationObserver(() => {
const targetElement = document.querySelector(targetSelector);
if (targetElement && !inputInserted) {
console.log("Inserting inputContainer...");
targetElement.appendChild(inputContainer);
inputInserted = true; // 标记已插入
observer.disconnect(); // 停止监听
}
});
// 开始观察 DOM 的变化
observer.observe(document.body, { childList: true, subtree: true });
// 添加超时时间,避免卡住
setTimeout(() => {
if (!inputInserted) {
console.error("Timeout: Element not found, appending to <body>");
inputContainer.style.width = '300px';
inputContainer.style.position = 'fixed';
inputContainer.style.top = '5px'
inputContainer.style.left = '49%';
inputContainer.style.transform = 'translate(-50%, 0%)';
document.body.appendChild(inputContainer);
observer.disconnect();
}
}, 5000); // 视情况调整时间
// 3 按钮事件函数
// 3.1 开始按钮事件
startButton.addEventListener('click', () => {
const sequence = sequenceInput.value;
if (sequence.trim()) {
try {
const steps = parseSequence(sequence);
isRunning = true;
toggleButtons(true); // 禁用按钮
startPomodoroCycles();
minimizeButton.click();
} catch (error) {
alert(error.message);
}
} else {
alert('Please enter a valid sequence.');
}
});
// 3.2 结束按钮事件
endButton.addEventListener('click', () => {
stopPomodoro();
toggleButtons(false); // 重新启用按钮
// 新增Squash逻辑
const squashButton = findButtonByText("Squash");
const stopButton = findButtonByText("Stop")
if (squashButton) {
simulateUserClick(squashButton);
console.log("Squash按钮已点击");
} else {
simulateUserClick(stopButton);
console.log("Stop按钮已点击");
}
alert("已停止番茄钟并关闭当前任务");
});
// 3.3 缩小按钮事件
let isMinimized = false;
minimizeButton.addEventListener('click', () => {
isMinimized = !isMinimized;
if (isMinimized) {
// 隐藏其他内容
sequenceRow.style.display = 'none';
hintText.style.display = 'none';
buttonRow.style.display = 'none';
minimizeButton.textContent = '+';
} else {
// 显示所有内容
sequenceRow.style.display = 'flex';
hintText.style.display = 'block';
buttonRow.style.display = 'flex';
minimizeButton.textContent = '-';
}
});
// 3.4 番茄点击事件处理
addPomodoroButton.addEventListener('click', () => {
// 获取当前文本
let currentText = sequenceInput.value.trim();
// 添加新的番茄序列
const newSequence = '+🍅+05';
// 如果当前文本为空,则不需要添加开头的+号
if (currentText === '') {
sequenceInput.value = '🍅+05';
} else {
sequenceInput.value = currentText + newSequence;
}
});
// 3.5 pizza点击事件
addPizzaButton.addEventListener('click', () => {
// 获取当前文本
let currentText = sequenceInput.value.trim();
// 添加新的番茄序列
const newSequence = '+🍅+05+🍅+05+🍅+05+🍅+15';
// 如果当前文本为空,则不需要添加开头的+号
if (currentText === '') {
sequenceInput.value = '🍅+05+🍅+05+🍅+05+🍅+15';
} else {
sequenceInput.value = currentText + newSequence;
}
});
// 3.6 在开始和结束时控制按钮状态的函数
function toggleButtons(disabled) {
addPomodoroButton.disabled = disabled;
addPizzaButton.disabled = disabled;
startButton.disabled = disabled;
sequenceInput.disabled = disabled; // 同时禁用输入框
// 设置禁用时的样式
[addPomodoroButton, addPizzaButton, startButton].forEach(button => {
if (disabled) {
button.style.opacity = '0.5';
button.style.cursor = 'not-allowed';
button.setAttribute('data-tooltip', '你想点啥?干活🤡'); // 添加提示文本
} else {
button.style.opacity = '1';
button.style.cursor = 'pointer';
button.removeAttribute('data-tooltip'); // 移除提示文本
}
});
// 设置输入框禁用时的样式
if (disabled) {
sequenceInput.style.opacity = '0.7';
sequenceInput.style.cursor = 'not-allowed';
} else {
sequenceInput.style.opacity = '1';
sequenceInput.style.cursor = 'text';
}
}
// 4 动画
// 4.1 创建时间块的函数
function createTimeBlock(duration, type) {
const block = document.createElement('div');
// 设置宽度比例 (25分钟番茄 : 100px, 5分钟休息 : 20px)
const width = type === 'work' ? 100 : (duration * 4);
block.style.cssText = `
height: 20px;
width: ${width}px;
background-color: ${type === 'work' ? '#e5d3d3' : '#d3e5d3' }; // 灰化的绿色和红色
border-radius: 3px;
transition: all 0.3s;
`;
return block;
}
// 4.2 更新进度条状态的函数
function updateProgressStatus(currentStep) {
const blocks = progressContainer.children;
Array.from(blocks).forEach((block, index) => {
if (index < currentStep) {
// 已完成的块
block.style.backgroundColor = block.classList.contains('work')
? '#f44336' // 鲜红色
: '#4CAF50'; // 鲜绿色
} else if (index === currentStep) {
// 当前执行的块,使用鲜艳的动画
block.style.backgroundImage = 'linear-gradient(45deg, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent)';
block.style.backgroundSize = '20px 20px';
block.style.animation = 'progress-animation 1s linear infinite';
block.style.backgroundColor = block.classList.contains('work') ? '#f76d6d' : '#4CD6A4'; // 更鲜艳的颜色
} else {
// 未完成的块,保持工作或休息的颜色
block.style.backgroundColor = ''; // 清除背景颜色,以便使用类定义的颜色
block.style.animation = 'none'; // 移除动画
if (block.classList.contains('work')) {
block.style.backgroundColor = 'rgba(244, 67, 54, 0.2)'; // 休息状态为灰色
} else {
block.style.backgroundColor = 'rgba(76, 175, 80, 0.2)'; // 工作状态为灰色
}
}
});
}
document.head.appendChild(style);
// 4.3 解析序列并创建进度块
function initializeProgress(sequence) {
progressContainer.innerHTML = ''; // 清空现有内容
const steps = parseSequence(sequence);
steps.forEach(step => {
const block = createTimeBlock(step.duration, step.type);
block.classList.add(step.type); // 添加类型标记
progressContainer.appendChild(block);
});
}
})();

----ฅ(*ΦωΦ)ฅ---- cognata ad sidera tendit...
浙公网安备 33010602011771号