Threads 愛心板

支援多目標、自訂首頁、倒數顯示、防偵測與隨機按愛心功能,按愛心機率提升到60%,優化時間並正確計算完整循環。

// ==UserScript==
// @name         Threads 愛心板
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  支援多目標、自訂首頁、倒數顯示、防偵測與隨機按愛心功能,按愛心機率提升到60%,優化時間並正確計算完整循環。
// @author       ChatGPT
// @match        https://www.threads.net/*
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';
    // --- 防偵測 ---
    Object.defineProperty(navigator, 'webdriver', {get:() => false});
    Object.defineProperty(navigator, 'languages', {get:() => ['zh-TW','zh']});
    Object.defineProperty(navigator, 'plugins', {get:() => [1,2,3,4,5]});
    const UA_SUFFIX = [
        ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.96 Mobile Safari/537.36',
        ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.90 Mobile Safari/537.36',
        ' AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6082.47 Mobile Safari/537.36'
    ];
    const baseUA = navigator.userAgent.replace(/\s*\(.+?\)/, '');
    Object.defineProperty(navigator, 'userAgent', {get:() => baseUA + UA_SUFFIX[Math.floor(Math.random()*UA_SUFFIX.length)]});

    // --- 存儲鍵 ---
    const NS = 'ThreadsV24_';
    const STORAGE = {
        TARGETS: NS+'TARGETS',
        IDX:     NS+'IDX',
        HOME:    NS+'HOME',
        RUN:     NS+'RUN',
        LOOP:    NS+'LOOP',
        MAX:     NS+'MAX'
    };
    const DEFAULT_HOME = 'https://www.threads.net';
    const DEFAULT_TARGETS = ['https://www.threads.net/posts/xxxxxx'];

    // --- 時間設定 (毫秒) ---
    const R = {
        STAY:   [20000,30000],  // 目標頁停留
        BROWSE: [60000,90000],  // 首頁瀏覽
        SCROLL: [1000,3000],    // 滾動間隔
        PAUSE:  [3000,10000]    // 跳轉等待
    };
    const rand = (a,b) => Math.floor(Math.random()*(b-a+1)) + a;
    const wait = ms => new Promise(r => setTimeout(r,ms));
    const save = (k,v) => GM_setValue(k,v);
    const load = (k,d) => { const v = GM_getValue(k); return v!==undefined ? v : d; };

    // --- 通知與 Toast ---
    function notify(title,text){
        if(typeof GM_notification==='function') GM_notification({title,text,timeout:2000});
        else toast(`${title}: ${text}`);
    }
    function toast(msg){
        const d = document.createElement('div'); d.textContent = msg;
        Object.assign(d.style,{position:'fixed',bottom:'20px',left:'50%',transform:'translateX(-50%)',background:'rgba(0,0,0,0.7)',color:'#fff',padding:'6px 10px',borderRadius:'4px',zIndex:99999});
        document.body.appendChild(d);
        setTimeout(()=>d.remove(),2000);
    }

    // --- UI ---
    const PID = 'tm_threads_v24'; if(document.getElementById(PID)) return;
    const panel = document.createElement('div'); panel.id = PID;
    Object.assign(panel.style,{position:'fixed',top:'10px',right:'10px',background:'#fff',border:'1px solid #ccc',padding:'8px',fontSize:'13px',zIndex:9999});
    const btnStart = document.createElement('button'); btnStart.textContent='開始';
    const btnStop  = document.createElement('button'); btnStop.textContent='暫停'; btnStop.style.marginLeft='6px';
    const inpMax   = document.createElement('input'); inpMax.placeholder='最大循環次數(留空無限)'; inpMax.style.width='100%'; inpMax.style.margin='4px 0';
    const inpHome  = document.createElement('input'); inpHome.placeholder='首頁 URL'; inpHome.style.width='100%'; inpHome.style.margin='4px 0';
    const inpTar   = document.createElement('textarea'); inpTar.placeholder='多目標 URL (逗號分隔)'; inpTar.style.width='100%'; inpTar.style.height='50px'; inpTar.style.margin='4px 0';
    const lblStatus= document.createElement('div'); lblStatus.textContent='狀態: 待命';
    const lblLoop  = document.createElement('div'); lblLoop.textContent='完整循環: 0';
    const lblCount = document.createElement('div'); lblCount.textContent='倒數: 0s';
    panel.append(btnStart,btnStop,inpMax,inpHome,inpTar,lblStatus,lblLoop,lblCount);
    document.body.appendChild(panel);
    const updateStatus = t => lblStatus.textContent='狀態: '+t;

    // --- 初始化值 ---
    inpHome.value = load(STORAGE.HOME,DEFAULT_HOME);
    inpTar.value  = load(STORAGE.TARGETS,JSON.stringify(DEFAULT_TARGETS)).replace(/[\[\]"]+/g,'').split(',').join(', ');

    // --- 取值與狀態 ---
    function getTargets(){ try{ return JSON.parse(load(STORAGE.TARGETS,JSON.stringify(DEFAULT_TARGETS))); } catch { return DEFAULT_TARGETS; } }
    function getNext(){ const list = getTargets(); let idx = load(STORAGE.IDX,-1); idx = (idx+1)%list.length; save(STORAGE.IDX,idx); return list[idx]; }
    function isTarget(){ return getTargets().some(u=>location.href.startsWith(u)); }

    // 紀錄進入目標頁
    let visitedTarget = false;

    // --- 滾動模擬 ---
    async function scrollOnce(){ const step = window.innerHeight*(Math.random()*0.5+0.5); let y = Math.min(Math.max(0,window.scrollY+step*(Math.random()<0.7?1:-1)), document.documentElement.scrollHeight-window.innerHeight); window.scrollTo({top:y,behavior:'smooth'}); await wait(rand(...R.SCROLL)); }

    // --- 單次模擬 ---
    async function simulate(){
        const tgt = isTarget();
        // 首頁隨機按愛心 60% 機率
        if(!tgt && Math.random()<0.6){
            const hearts = document.querySelectorAll('button[aria-label="Like"], button[aria-label="喜歡"]');
            if(hearts.length){
                hearts[Math.floor(Math.random()*hearts.length)].click();
                notify('模擬','隨機按愛心');
                await wait(rand(...R.PAUSE)/2);
            }
        }
        if(tgt) visitedTarget = true;

        updateStatus('瀏覽 '+(tgt?'目標頁':'首頁'));
        notify('模擬','開始瀏覽 '+(tgt?'目標頁':'首頁'));

        const duration = rand(...(tgt?R.STAY:R.BROWSE));
        let rem = duration;
        lblCount.textContent = '倒數: '+Math.ceil(rem/1000)+'s';
        const ti = setInterval(()=>{ rem -= 1000; lblCount.textContent = '倒數: '+(rem>0?Math.ceil(rem/1000):0)+'s'; }, 1000);
        const t0 = Date.now();
        while(Date.now()-t0<duration && load(STORAGE.RUN,false)) await scrollOnce();
        clearInterval(ti); lblCount.textContent = '倒數: 0s';
        if(!load(STORAGE.RUN,false)) return;

        await wait(rand(...R.PAUSE));
        const homeUrl = inpHome.value.trim()||DEFAULT_HOME; save(STORAGE.HOME,homeUrl);
        const nextUrl = tgt? homeUrl : getNext();
        // 完整循環:從目標頁返回首頁時計數一次
        if(tgt && nextUrl===homeUrl && visitedTarget){
            let c = load(STORAGE.LOOP,0) + 1; save(STORAGE.LOOP,c); lblLoop.textContent = '完整循環: '+c;
            const m = load(STORAGE.MAX,Infinity); if(m!==Infinity && c>=m){ stop(`達上限 ${m}`); return;} visitedTarget = false;
        }
        location.href = nextUrl;
    }

    // --- 頁面載入自動恢復 ---
    function autoContinue(){ if(load(STORAGE.RUN,false)){ btnStart.disabled=true; lblLoop.textContent='完整循環: '+load(STORAGE.LOOP,0); simulate(); }}
    window.addEventListener('load',()=>setTimeout(autoContinue,3000));

    // --- 迴圈執行 ---
    async function runner(){ while(load(STORAGE.RUN,false)) await simulate(); }

    function stop(msg){ save(STORAGE.RUN,false); updateStatus('已停止'); notify('停止',msg||'已停止'); btnStart.disabled=false; }

    // --- 按鈕事件 ---
    btnStart.onclick = ()=>{
        const m = parseInt(inpMax.value); if(!isNaN(m)) save(STORAGE.MAX,m);
        const arr = inpTar.value.split(',').map(s=>s.trim()).filter(s=>s); if(arr.length) save(STORAGE.TARGETS,JSON.stringify(arr));
        save(STORAGE.IDX,-1); save(STORAGE.LOOP,0); save(STORAGE.RUN,true); visitedTarget=false;
        lblLoop.textContent = '完整循環: 0'; updateStatus('運行中'); btnStart.disabled=true; runner();
    };
    btnStop.onclick = ()=>stop('手動停止');
})();