连续番茄时钟和长休息

  1. 去支持Tempermonkey的浏览器的Add-ons安装
  2. 代码
  3. 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);
        });
    }



})();
posted @ 2025-01-16 15:07  Xeonilian  阅读(261)  评论(0)    收藏  举报