iClick

Auto-click any button on any page. Supports interval, time limit, click limit, and persistent run across page navigation.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         iClick
// @namespace    https://github.com/
// @version      2.1
// @description  Auto-click any button on any page. Supports interval, time limit, click limit, and persistent run across page navigation.
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
  'use strict';

  // ── 暗色模式 ──
  const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  const C = dark
    ? { bg: '#1f1f1f', border: '#333', text: '#ccc', sub: '#888', badge: '#2a2a2a', sep: '#333' }
    : { bg: '#fff',    border: '#ddd', text: '#333', sub: '#555', badge: '#f5f5f5', sep: '#eee' };

  // ── 共享顶栏 ──
  function getTopbar() {
    let bar = document.getElementById('kiro-topbar');
    if (!bar) {
      bar = document.createElement('div');
      bar.id = 'kiro-topbar';
      bar.style.cssText = `position:fixed;top:0;left:0;right:0;z-index:99999;background:${C.bg};border-bottom:1px solid ${C.border};display:flex;align-items:stretch;min-height:36px;box-shadow:0 2px 6px rgba(0,0,0,.15);font-size:13px;font-family:sans-serif;`;
      document.body.appendChild(bar);
      document.body.style.paddingTop = '36px';
    }
    return bar;
  }

  function addSection(id) {
    const bar = getTopbar();
    if (bar.querySelector(`#${id}`)) return bar.querySelector(`#${id}`);
    if (bar.children.length > 0) {
      const sep = document.createElement('div');
      sep.style.cssText = `width:1px;background:${C.sep};margin:6px 0;flex-shrink:0;`;
      bar.appendChild(sep);
    }
    const spacer = document.createElement('div');
    spacer.style.cssText = 'flex:1;';
    bar.appendChild(spacer);
    const sec = document.createElement('div');
    sec.id = id;
    sec.style.cssText = 'display:flex;align-items:center;gap:8px;padding:0 14px;flex-shrink:0;overflow:hidden;';
    bar.appendChild(sec);
    return sec;
  }

  // ── 跨页持久运行:页面加载时检查是否应自动恢复 ──
  const PERSIST_KEY = 'ac_persist';
  const savedState = JSON.parse(GM_getValue(PERSIST_KEY, 'null'));
  const shouldResume = savedState && savedState.running && savedState.target;

  const savedInterval = GM_getValue('ac_interval', 1500);

  let running = false, timer = null, clockTimer = null;
  let count = 0, elapsed = 0;

  // ── UI ──
  const sec = addSection('kiro-clicker');

  const label = el('span', `font-weight:700;white-space:nowrap;color:${C.text};font-size:13px;letter-spacing:.5px;`, 'iClick');

  const targetSelect = document.createElement('select');
  targetSelect.style.cssText = `padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};max-width:120px;flex-shrink:0;outline:none;cursor:pointer;`;

  const intervalInput = document.createElement('input');
  intervalInput.type = 'number'; intervalInput.value = savedInterval; intervalInput.min = 100;
  intervalInput.title = '间隔(ms)';
  intervalInput.style.cssText = `width:64px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;

  const limitInput = document.createElement('input');
  limitInput.type = 'number'; limitInput.value = GM_getValue('ac_limit', 0); limitInput.min = 0;
  limitInput.title = '时限(分钟,0=不限)';
  limitInput.style.cssText = `width:54px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;

  const maxInput = document.createElement('input');
  maxInput.type = 'number'; maxInput.value = GM_getValue('ac_max', 0); maxInput.min = 0;
  maxInput.title = '点击上限(0=不限)';
  maxInput.style.cssText = `width:54px;padding:2px 8px;border:1px solid ${C.border};border-radius:12px;font-size:12px;background:${C.badge};color:${C.text};outline:none;text-align:center;`;

  const status = el('span', `color:${C.sub};white-space:nowrap;font-size:12px;`, '就绪');
  const toggleBtn = btn('▶ 开始', '#1677ff');

  const lbInterval = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'ms');
  const lbLimit    = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'min');
  const lbMax      = el('span', `color:${C.sub};font-size:11px;white-space:nowrap;`, 'max');

  sec.append(label, targetSelect, lbInterval, intervalInput, lbLimit, limitInput, lbMax, maxInput, status, toggleBtn);

  // 扫描按钮
  const refreshButtons = () => {
    const cur = targetSelect.value || (savedState && savedState.target) || '';
    targetSelect.innerHTML = '';
    [...new Set(
      [...document.querySelectorAll('button,input[type=button],input[type=submit],[role=button]')]
        .filter(b => b.offsetParent && !document.getElementById('kiro-clicker')?.contains(b))
        .map(b => (b.textContent || b.value || '').trim())
        .filter(Boolean)
    )].forEach(text => {
      const o = document.createElement('option');
      o.value = o.textContent = text;
      if (text === cur) o.selected = true;
      targetSelect.appendChild(o);
    });
  };
  refreshButtons();
  targetSelect.onfocus = refreshButtons;

  const findTarget = () =>
    [...document.querySelectorAll('button,input[type=button],input[type=submit],[role=button]')]
      .find(b => (b.textContent || b.value || '').trim() === targetSelect.value
        && !b.disabled && b.offsetParent
        && !document.getElementById('kiro-clicker')?.contains(b));

  const stop = (reason) => {
    running = false;
    clearInterval(timer); clearInterval(clockTimer);
    toggleBtn.textContent = '▶ 开始'; toggleBtn.style.background = '#1677ff';
    GM_setValue(PERSIST_KEY, JSON.stringify({ running: false, target: targetSelect.value }));
    if (reason) status.textContent = reason;
  };

  const start = () => {
    const interval = Math.max(100, parseInt(intervalInput.value) || 1500);
    const limitSec = (parseFloat(limitInput.value) || 0) * 60;
    const maxClicks = parseInt(maxInput.value) || 0;
    GM_setValue('ac_interval', interval);
    GM_setValue('ac_limit', limitInput.value);
    GM_setValue('ac_max', maxInput.value);
    GM_setValue(PERSIST_KEY, JSON.stringify({ running: true, target: targetSelect.value }));

    count = 0; elapsed = 0; running = true;
    toggleBtn.textContent = '⏹ 停止'; toggleBtn.style.background = '#ff4d4f';

    clockTimer = setInterval(() => {
      elapsed++;
      const h = Math.floor(elapsed / 3600), m = Math.floor((elapsed % 3600) / 60), s = elapsed % 60;
      const timeStr = `${h ? h + 'h ' : ''}${m ? m + 'm ' : ''}${s}s`;
      status.textContent = `已点击 ${count} 次 · ${timeStr}`;
      if (limitSec > 0 && elapsed >= limitSec) stop('⏱ 已达时限,自动停止');
    }, 1000);

    timer = setInterval(() => {
      const el = findTarget();
      if (el) {
        el.click(); count++;
        if (maxClicks > 0 && count >= maxClicks) stop(`✅ 已达上限 ${maxClicks} 次,自动停止`);
      } else {
        status.textContent = `⚠️ 未找到按钮「${targetSelect.value}」`;
      }
    }, interval);
  };

  toggleBtn.onclick = () => running ? stop() : start();

  // 自动恢复
  if (shouldResume) {
    // 等 DOM 稳定后恢复
    setTimeout(() => { refreshButtons(); start(); status.textContent = '已自动恢复运行'; }, 1500);
  }

  function btn(text, color) {
    const b = document.createElement('button');
    b.textContent = text;
    b.style.cssText = `padding:3px 14px;background:${color};color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:12px;white-space:nowrap;flex-shrink:0;font-weight:500;letter-spacing:.3px;transition:opacity .15s;`;
    b.onmouseover = () => b.style.opacity = '0.8';
    b.onmouseout  = () => b.style.opacity = '1';
    return b;
  }

  function el(tag, css, text) {
    const e = document.createElement(tag);
    e.style.cssText = css;
    if (text !== undefined) e.textContent = text;
    return e;
  }
})();