Greasy Fork

Greasy Fork is available in English.

🛡️定时点击

支持启动自动最小化 | 默认位置记忆 | 代码预设 | 双击启停 | 完美吸附

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         🛡️定时点击
// @namespace    http://tampermonkey.net/
// @version      15.0
// @description  支持启动自动最小化 | 默认位置记忆 | 代码预设 | 双击启停 | 完美吸附
// @author       Geek_Omni
// @license      MIT
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // =================================================================
    // 🔧 用户自定义配置区 (在这里修改默认行为)
    // =================================================================

    // 1. [开关] 刷新页面后是否自动加载插件?
    // true = 自动加载 (配合下方配置可实现自动最小化)
    // false = 完全不加载 (需去菜单手动开启)
    const AUTO_SHOW_GUI = true;

    // 2. [开关] 启动时是否自动最小化为悬浮胶囊?
    // true = 启动即最小化 (只显示小圆球/胶囊)
    // false = 启动显示完整大面板
    const AUTO_MINIMIZE = true;

    // 3. [预设] 常用的按钮选择器 (重置或首次运行时自动加载)
    const USER_PRESETS = [
        { name: "天成保活", path: "#app > div > div.main-container > section > div > div:nth-child(4) > div:nth-child(4) > form > div:nth-child(4) > div > button.el-button.el-button--primary.el-button--mini" },
        // { name: "导出", path: ".export-data" }
    ];

    // =================================================================

    // --- 0. 核心配置 ---
    const CONFIG_KEY = 'geek_config_v15';
    const STATE_KEY = 'geek_hud_visible_v15_force';
    const POS_KEY = 'geek_hud_position_v15';

    let config = GM_getValue(CONFIG_KEY, {
        minSeconds: 300,
        maxSeconds: 360,
        targets: USER_PRESETS
    });

    // 读取状态:如果存储中没有记录,则使用 AUTO_SHOW_GUI 的值
    let isHudVisible = GM_getValue(STATE_KEY, AUTO_SHOW_GUI);
    let lastPos = GM_getValue(POS_KEY, { top: '50px', left: 'auto', right: '50px' });

    let timerId = null;
    let remaining = 0;
    let isPaused = true;
    let hud, miniPill;

    // --- 1. 扩展菜单集成 ---
    GM_registerMenuCommand("🖥️ 开启/关闭控制台", () => {
        isHudVisible = !isHudVisible;
        GM_setValue(STATE_KEY, isHudVisible);
        if (isHudVisible) initUI();
        else destroyUI();
    });

    if (isHudVisible) initUI();

    // --- 2. UI 构建 ---
    function initUI() {
        if (document.getElementById('geek-hud-main')) return;

        GM_addStyle(`
            #geek-hud-main, #geek-hud-mini { font-family: 'Segoe UI', Consolas, monospace; user-select: none; z-index: 2147483647; box-sizing: border-box; }
            .geek-btn { cursor: pointer; border: none; outline: none; transition: filter 0.2s; }
            .geek-btn:hover { filter: brightness(1.2); }
            .geek-input { background: #1a1a1a; color: #ddd; border: 1px solid #444; border-radius: 4px; padding: 4px; outline: none; }
        `);

        // A. 主面板
        hud = document.createElement('div');
        hud.id = 'geek-hud-main';
        hud.style.cssText = `
            position: fixed; width: 220px;
            top: ${lastPos.top}; left: ${lastPos.left}; right: ${lastPos.right};
            background: rgba(20, 20, 20, 0.98); border: 1px solid #444;
            color: #00ff00; border-radius: 6px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.8);
            display: flex; flex-direction: column;
        `;

        hud.innerHTML = `
            <div id="geek-header" style="height: 36px; background: #004d00; cursor: move; display: flex; justify-content: space-between; align-items: center; padding: 0 10px; border-radius: 6px 6px 0 0; border-bottom: 1px solid #333;">
                <span style="font-size: 13px; font-weight: 700; color: #fff;">🛡️ 保活 V15</span>
                <div style="display:flex; gap:10px;">
                    <span id="geek-btn-min" class="geek-btn" style="color: #fff; font-size:14px;" title="最小化">➖</span>
                    <span id="geek-btn-close" class="geek-btn" style="color: #ff6666; font-size:14px;" title="关闭">✕</span>
                </div>
            </div>
            <div style="padding: 15px; display: flex; flex-direction: column; gap: 12px;">
                <div style="display: flex; gap: 5px;">
                    <select id="geek-selector" class="geek-input" style="flex: 1;"><option value="-1">-- 未配置 --</option></select>
                    <button id="geek-btn-add" class="geek-btn" style="width: 28px; background: #28a745; color: white; border-radius: 4px;" title="添加">+</button>
                    <button id="geek-btn-del" class="geek-btn" style="width: 28px; background: #d00; color: white; border-radius: 4px;" title="删除">🗑️</button>
                </div>
                <div id="geek-timer" style="font-size: 32px; font-weight: 700; text-align: center; color: #666; letter-spacing: 2px;">OFF</div>
                <div style="display: flex; gap: 8px; margin-top: 5px;">
                    <button id="geek-btn-control" class="geek-btn" style="flex: 1; background: #333; color: #aaa; padding: 8px 0; font-weight: bold; border-radius: 4px;">▶ 启动</button>
                    <button id="geek-btn-setting" class="geek-btn" style="width: 36px; background: #333; color: white; border-radius: 4px;" title="设置">⚙</button>
                </div>
            </div>
        `;

        // B. 悬浮胶囊
        miniPill = document.createElement('div');
        miniPill.id = 'geek-hud-mini';
        miniPill.title = "双击此处 启动/暂停";
        miniPill.style.cssText = `
            position: fixed; top: 0; left: 0; padding: 5px 15px 5px 5px;
            background: rgba(0, 0, 0, 0.95); border-radius: 20px;
            border: 1px solid #555; display: none; align-items: center; gap: 10px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.6); cursor: move; transition: border-color 0.3s;
        `;
        miniPill.innerHTML = `
            <div id="geek-mini-restore" class="geek-btn" style="width: 26px; height: 26px; background: #007bff; border-radius: 50%; display: flex; justify-content: center; align-items: center; color: white; font-weight: bold;" title="还原界面">⤢</div>
            <span id="geek-mini-text" style="font-size: 14px; font-weight: bold; color: #888; min-width: 45px; text-align: center;">OFF</span>
        `;

        document.body.appendChild(hud);
        document.body.appendChild(miniPill);

        // 1. 先规范化坐标,确保 left/top 有具体数值
        normalizeCoordinates(hud);

        // 2. 根据 AUTO_MINIMIZE 决定初始状态 [✅关键新增逻辑]
        if (AUTO_MINIMIZE) {
            hud.style.display = 'none';
            miniPill.style.display = 'flex';

            // 计算胶囊位置 (基于主窗口位置 + 偏移)
            const hLeft = parseFloat(hud.style.left);
            const hTop = parseFloat(hud.style.top);
            const hWidth = 220;

            // 让胶囊出现在主窗口原本应该在的右侧区域
            let pLeft = hLeft + hWidth - 120;
            if(pLeft < 0) pLeft = 0;

            miniPill.style.top = hTop + 'px';
            miniPill.style.left = pLeft + 'px';
            ensureInViewport(miniPill);
        }

        bindEvents();
        renderSelector();
        updateUI();

        setupSafeDrag(hud, document.getElementById('geek-header'));
        setupSafeDrag(miniPill, miniPill);
    }

    function destroyUI() {
        if (hud) hud.remove();
        if (miniPill) miniPill.remove();
        isPaused = true;
        clearInterval(timerId);
        timerId = null;
        hud = null; miniPill = null;
    }

    // --- 3. 逻辑控制 ---
    function bindEvents() {
        const dom = getDom();

        dom.miniPill.ondblclick = (e) => {
            if (e.target.id !== 'geek-mini-restore' && !e.target.closest('#geek-mini-restore')) {
                toggleRun();
                dom.miniPill.style.borderColor = isPaused ? '#555' : '#00ff00';
                setTimeout(() => dom.miniPill.style.borderColor = '#555', 300);
            }
        };

        dom.btnMin.onclick = () => {
            const rect = hud.getBoundingClientRect();
            hud.style.display = 'none';
            miniPill.style.display = 'flex';
            const pillLeft = rect.right - 100;
            miniPill.style.top = rect.top + 'px';
            miniPill.style.left = (pillLeft > 0 ? pillLeft : 0) + 'px';
            miniPill.style.right = 'auto';
            ensureInViewport(miniPill);
            updateUI();
        };

        dom.miniRestore.onclick = (e) => {
            e.stopPropagation();
            const rect = miniPill.getBoundingClientRect();
            miniPill.style.display = 'none';
            hud.style.display = 'flex';
            hud.style.top = rect.top + 'px';
            hud.style.left = (rect.left - 160) + 'px';
            hud.style.right = 'auto';
            ensureInViewport(hud);
            savePosition(hud);
        };

        dom.btnClose.onclick = () => {
            isHudVisible = false;
            GM_setValue(STATE_KEY, false);
            destroyUI();
        };

        dom.btnAdd.onclick = () => {
            const name = prompt("备注:", "查询"); if(!name) return;
            const path = prompt("Selector:", ""); if(!path) return;
            config.targets.push({ name, path: path.trim() });
            GM_setValue(CONFIG_KEY, config); renderSelector(config.targets.length - 1);
        };

        dom.btnDel.onclick = () => {
            const idx = parseInt(dom.selector.value);
            if(idx >= 0 && confirm("删除?")) {
                config.targets.splice(idx, 1);
                GM_setValue(CONFIG_KEY, config); renderSelector(0);
            }
        };

        dom.btnControl.onclick = toggleRun;
        dom.btnSetting.onclick = () => {
            const input = prompt(`间隔 (s):`, `${config.minSeconds},${config.maxSeconds}`);
            if (input) {
                const [min, max] = input.split(/[,,]/).map(Number);
                if (min && max) {
                    config.minSeconds = min; config.maxSeconds = max;
                    GM_setValue(CONFIG_KEY, config);
                    if(!isPaused) resetTimer();
                }
            }
        };

        if(!timerId) timerId = setInterval(tick, 1000);
    }

    function toggleRun() {
        if (config.targets.length === 0) return alert("⚠️ 请先添加按钮!");
        isPaused = !isPaused;
        if (!isPaused && remaining <= 0) resetTimer();
        updateUI();
    }

    function tick() {
        if(isPaused || !hud) return;
        const dom = getDom();
        dom.timer.innerText = formatTime(remaining);
        dom.miniText.innerText = formatTime(remaining);

        if (remaining <= 0) {
            const idx = parseInt(dom.selector.value);
            const target = config.targets[idx];
            if(target) {
                const el = document.querySelector(target.path);
                if(el) {
                    el.click();
                    dom.miniText.style.color = "#0f0";
                    setTimeout(()=> { if(!isPaused) dom.miniText.style.color = "#0f0"; }, 500);
                }
            }
            resetTimer();
        } else {
            remaining--;
        }
    }

    // --- 4. 拖拽引擎 ---
    function setupSafeDrag(el, handle) {
        let isDragging = false;
        let startX, startY, initialLeft, initialTop;
        handle.addEventListener('mousedown', (e) => {
            if (e.button !== 0) return;
            isDragging = true;
            const rect = el.getBoundingClientRect();
            initialLeft = rect.left;
            initialTop = rect.top;
            startX = e.clientX;
            startY = e.clientY;

            el.style.right = 'auto';
            el.style.bottom = 'auto';
            el.style.width = el.offsetWidth + 'px';
            el.style.cursor = 'grabbing';
            handle.style.cursor = 'grabbing';

            const onMove = (mv) => {
                if (!isDragging) return;
                mv.preventDefault();
                let newLeft = initialLeft + (mv.clientX - startX);
                let newTop = initialTop + (mv.clientY - startY);
                const maxLeft = window.innerWidth - el.offsetWidth;
                const maxTop = window.innerHeight - el.offsetHeight;
                newLeft = Math.min(Math.max(0, newLeft), maxLeft);
                newTop = Math.min(Math.max(0, newTop), maxTop);
                el.style.left = newLeft + 'px';
                el.style.top = newTop + 'px';
            };
            const onUp = () => {
                if(isDragging) {
                    isDragging = false;
                    el.style.cursor = 'auto';
                    handle.style.cursor = 'move';
                    if (el.id === 'geek-hud-main') savePosition(el);
                }
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
            };
            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onUp);
        });
    }

    function normalizeCoordinates(el) {
        const rect = el.getBoundingClientRect();
        el.style.left = rect.left + 'px';
        el.style.top = rect.top + 'px';
        el.style.right = 'auto';
        el.style.bottom = 'auto';
    }

    function ensureInViewport(el) {
        const rect = el.getBoundingClientRect();
        let newLeft = rect.left;
        let newTop = rect.top;
        if (newLeft + rect.width > window.innerWidth) newLeft = window.innerWidth - rect.width - 10;
        if (newLeft < 0) newLeft = 10;
        if (newTop + rect.height > window.innerHeight) newTop = window.innerHeight - rect.height - 10;
        if (newTop < 0) newTop = 10;
        el.style.left = newLeft + 'px';
        el.style.top = newTop + 'px';
        el.style.right = 'auto';
    }

    function savePosition(el) {
        GM_setValue(POS_KEY, { top: el.style.top, left: el.style.left, right: 'auto' });
    }

    function getDom() {
        return {
            selector: document.getElementById('geek-selector'),
            timer: document.getElementById('geek-timer'),
            btnAdd: document.getElementById('geek-btn-add'),
            btnDel: document.getElementById('geek-btn-del'),
            btnControl: document.getElementById('geek-btn-control'),
            btnSetting: document.getElementById('geek-btn-setting'),
            btnMin: document.getElementById('geek-btn-min'),
            btnClose: document.getElementById('geek-btn-close'),
            miniPill: document.getElementById('geek-hud-mini'),
            miniRestore: document.getElementById('geek-mini-restore'),
            miniText: document.getElementById('geek-mini-text')
        };
    }

    function updateUI() {
        const dom = getDom();
        if(!dom.timer) return;
        if (isPaused) {
            dom.btnControl.innerHTML = "▶ 启动";
            dom.btnControl.style.background = "#333";
            dom.btnControl.style.color = "#888";
            dom.timer.style.color = "#666";
            dom.miniText.style.color = "#888";
            dom.timer.innerText = "OFF";
            dom.miniText.innerText = "OFF";
        } else {
            dom.btnControl.innerHTML = "⏸ 运行";
            dom.btnControl.style.background = "#004d00";
            dom.btnControl.style.color = "#fff";
            dom.timer.style.color = "#00ff00";
            dom.miniText.style.color = "#00ff00";
        }
    }

    function renderSelector(idx = 0) {
        const sel = document.getElementById('geek-selector');
        sel.innerHTML = '';
        if (config.targets.length === 0) sel.appendChild(new Option("-- 未配置 --", -1));
        config.targets.forEach((t, i) => sel.appendChild(new Option(t.name, i)));
        sel.value = idx;
    }
    function resetTimer() {
        remaining = Math.floor(config.minSeconds + Math.random() * (config.maxSeconds - config.minSeconds + 1));
    }
    function formatTime(s) {
        if(s < 0) return "00:00";
        return `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
    }

})();