iClick

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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;
  }
})();