PT站候选自动投票

自动点击所有包含 vote=yeah 的链接。适用于岛等需要投票才能通过候选的站点。

// ==UserScript==
// @name         PT站候选自动投票
// @namespace    http://tampermonkey.net/
// @version      2025.06.18
// @description  自动点击所有包含 vote=yeah 的链接。适用于岛等需要投票才能通过候选的站点。
// @match        *://*/*offers.php*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    if (!window.location.href.toLowerCase().endsWith('offers.php')) {
        return;
    }

    // 添加自定义样式
    GM_addStyle(`
        .vote-dialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: #444;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 20px rgba(0,0,0,0.5);
            z-index: 10000;
            width: 300px;
            border: 1px solid #666;
            color: #fff;
            font-family: Arial, sans-serif;
        }
        .vote-dialog-title {
            font-size: 16px;
            font-weight: bold;
            margin-bottom: 15px;
            color: #fff;
        }
        .vote-dialog-content {
            margin-bottom: 20px;
            font-size: 14px;
        }
        .vote-dialog-button {
            padding: 8px 15px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            float: right;
        }
        .vote-dialog-button:hover {
            background-color: #45a049;
        }
    `);

    const config = {
        defaultDelay: 1000,
        timeout: 2500,
        panelBgColor: '#333',
        buttonSpacing: '5px',
        panelOpacity: 0.9 // 默认透明度改为 90%
    };

    // 获取当前网站域名
    const currentDomain = window.location.hostname;

    // 获取或初始化投票记录
    function getVoteHistory() {
        const allHistory = GM_getValue('voteHistory', {});
        return allHistory[currentDomain] || {};
    }

    // 保存投票记录
    function saveVoteHistory(history) {
        const allHistory = GM_getValue('voteHistory', {});
        allHistory[currentDomain] = history;
        GM_setValue('voteHistory', allHistory);
    }

    // 获取历史成功投票数
    function getSuccessfulVoteCount() {
        const history = getVoteHistory();
        return Object.values(history).filter(v => v === 'success').length;
    }

    // 记录成功的投票
    function recordSuccessfulVote(url) {
        const history = getVoteHistory();
        const id = extractIdFromUrl(url);
        history[id] = 'success';
        saveVoteHistory(history);
        updateHistoryCount();
    }

    // 从URL提取ID
    function extractIdFromUrl(url) {
        const match = url.match(/id=(\d+)/);
        return match ? match[1] : '未知ID';
    }

    // 显示自定义对话框
    function showDialog(message) {
        const dialog = document.createElement('div');
        dialog.className = 'vote-dialog';

        dialog.innerHTML = `
            <div class="vote-dialog-title">自动投票提示</div>
            <div class="vote-dialog-content">${message}</div>
            <button class="vote-dialog-button">确定</button>
        `;

        document.body.appendChild(dialog);

        dialog.querySelector('.vote-dialog-button').addEventListener('click', function() {
            document.body.removeChild(dialog);
        });
    }

    function createControlButton() {
        const toggleButton = document.createElement('button');
        toggleButton.id = 'voteToggleButton';
        toggleButton.textContent = '⚖️';
        toggleButton.style.position = 'fixed';
        toggleButton.style.top = '10px';
        toggleButton.style.right = '10px';
        toggleButton.style.zIndex = '9999';
        // 应用透明度到主按钮
        toggleButton.style.backgroundColor = `rgba(51, 51, 51, ${config.panelOpacity})`;
        toggleButton.style.padding = '5px 10px';
        toggleButton.style.borderRadius = '4px';
        toggleButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
        toggleButton.style.fontFamily = 'Arial, sans-serif';
        toggleButton.style.border = '1px solid #555';
        toggleButton.style.cursor = 'pointer';
        toggleButton.style.fontSize = '16px';

        document.body.appendChild(toggleButton);

        const panel = document.createElement('div');
        panel.id = 'autoVotePanel';
        panel.style.position = 'fixed';
        panel.style.top = '40px';
        panel.style.right = '10px';
        panel.style.zIndex = '9998';
        // 应用透明度到主面板
        panel.style.backgroundColor = `rgba(51, 51, 51, ${config.panelOpacity})`;
        panel.style.padding = '8px';
        panel.style.borderRadius = '4px';
        panel.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
        panel.style.fontFamily = 'Arial, sans-serif';
        panel.style.width = '200px';
        panel.style.border = '1px solid #555';
        panel.style.display = 'none';

        const title = document.createElement('div');
        title.textContent = '自动投票控制';
        title.style.margin = '0 0 8px 0';
        title.style.fontWeight = 'bold';
        title.style.fontSize = '13px';
        title.style.color = '#fff';

        // 历史投票数显示
        const historyCountText = document.createElement('div');
        historyCountText.id = 'historyVoteCount';
        historyCountText.style.margin = '0 0 8px 0';
        historyCountText.style.fontSize = '12px';
        historyCountText.style.color = '#fff';

        // 进度显示区域
        const progressContainer = document.createElement('div');
        progressContainer.style.marginBottom = '8px';

        const progressText = document.createElement('div');
        progressText.id = 'voteProgress';
        progressText.style.fontSize = '12px';
        progressText.style.color = '#fff';
        progressText.style.marginBottom = '4px';

        const progressBar = document.createElement('div');
        progressBar.id = 'voteProgressBar';
        progressBar.style.width = '100%';
        progressBar.style.height = '8px';
        progressBar.style.backgroundColor = '#555';
        progressBar.style.borderRadius = '4px';
        progressBar.style.overflow = 'hidden';

        const progressBarFill = document.createElement('div');
        progressBarFill.id = 'voteProgressBarFill';
        progressBarFill.style.height = '100%';
        progressBarFill.style.width = '0%';
        progressBarFill.style.backgroundColor = '#4CAF50';
        progressBarFill.style.transition = 'width 0.3s ease';

        progressBar.appendChild(progressBarFill);
        progressContainer.appendChild(progressText);
        progressContainer.appendChild(progressBar);

        const currentIdText = document.createElement('div');
        currentIdText.id = 'currentVoteId';
        currentIdText.style.margin = '0 0 8px 0';
        currentIdText.style.fontSize = '12px';
        currentIdText.style.color = '#ddd';
        currentIdText.textContent = '当前ID: -';

        const statsText = document.createElement('div');
        statsText.id = 'voteStats';
        statsText.style.margin = '0 0 8px 0';
        statsText.style.fontSize = '12px';
        statsText.style.color = '#aaa';

        const buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex';
        buttonContainer.style.flexDirection = 'column';
        buttonContainer.style.gap = config.buttonSpacing;

        const actionButton = document.createElement('button');
        actionButton.id = 'voteActionButton';
        actionButton.textContent = '开始';
        actionButton.style.padding = '4px 8px';
        actionButton.style.backgroundColor = '#4CAF50';
        actionButton.style.color = 'white';
        actionButton.style.border = 'none';
        actionButton.style.borderRadius = '3px';
        actionButton.style.cursor = 'pointer';
        actionButton.style.fontSize = '12px';
        actionButton.style.width = '100%';

        const settingsButton = document.createElement('button');
        settingsButton.textContent = '设置';
        // 统一按钮风格,和 “开始” 按钮大小一致
        settingsButton.style.padding = '4px 8px';
        settingsButton.style.backgroundColor = config.panelBgColor;
        settingsButton.style.color = 'white';
        settingsButton.style.border = '1px solid #555';
        settingsButton.style.borderRadius = '3px';
        settingsButton.style.cursor = 'pointer';
        settingsButton.style.fontSize = '12px';
        settingsButton.style.width = '100%';
        settingsButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
        settingsButton.style.border = '1px solid #555';
        settingsButton.style.cursor = 'pointer';
        settingsButton.style.fontSize = '12px';
        settingsButton.style.width = '100%';

        buttonContainer.appendChild(actionButton);
        buttonContainer.appendChild(settingsButton);

        panel.appendChild(title);
        panel.appendChild(historyCountText);
        panel.appendChild(progressContainer);
        panel.appendChild(currentIdText);
        panel.appendChild(statsText);
        panel.appendChild(buttonContainer);
        document.body.appendChild(panel);

        updateHistoryCount();
        updateProgress(0, 0, 0);
        updateStats(0, 0, 0);

        toggleButton.addEventListener('click', function() {
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        });

        actionButton.addEventListener('click', function() {
            if (actionButton.textContent === '开始') {
                startAutoVote();
                actionButton.textContent = '停止';
                actionButton.style.backgroundColor = '#f44336';
            } else {
                stopAutoVote();
                actionButton.textContent = '开始';
                actionButton.style.backgroundColor = '#4CAF50';
            }
        });

        settingsButton.addEventListener('click', showSettings);
    }

    // 更新历史成功投票数显示
    function updateHistoryCount() {
        const count = getSuccessfulVoteCount();
        const historyCountText = document.getElementById('historyVoteCount');
        if (historyCountText) {
            // 修改显示文本
            historyCountText.innerHTML = `本站历史投票<span style="color:#4CAF50">${count}</span>个`;
        }
    }

    function updateProgress(current, total, allVoteLinksCount) {
        const progressText = document.getElementById('voteProgress');
        const progressBarFill = document.getElementById('voteProgressBarFill');

        if (progressText && progressBarFill) {
            const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
            progressText.innerHTML = `进度: ${current}/${total} <span style="color:#4CAF50">(总:${allVoteLinksCount})</span>`;
            progressBarFill.style.width = `${percentage}%`;
        }
    }

    function updateCurrentId(id) {
        const currentIdText = document.getElementById('currentVoteId');
        if (currentIdText) {
            currentIdText.textContent = `当前ID: ${id}`;
        }
    }

    function updateStats(success, failed, skipped) {
        const statsText = document.getElementById('voteStats');
        if (statsText) {
            statsText.innerHTML = `
                <div>成功: <span style="color:#4CAF50">${success}</span></div>
                <div>超时: <span style="color:#f44336">${failed}</span></div>
                <div>跳过: <span style="color:#FF9800">${skipped}</span></div>
            `;
        }
    }

    function getVoteLinks() {
        const history = getVoteHistory();
        const allLinks = Array.from(document.querySelectorAll('a[href*="vote=yeah"]')).filter(link => {
            return link.href && !link.href.includes('#') && link.href !== window.location.href;
        });

        const allVoteLinksCount = allLinks.length;
        const votedLinks = [];
        const unvotedLinks = [];

        allLinks.forEach(link => {
            const id = extractIdFromUrl(link.href);
            if (history[id] === 'success') {
                votedLinks.push(link);
            } else {
                unvotedLinks.push(link);
            }
        });

        return {
            unvoted: unvotedLinks,
            skipped: votedLinks.length,
            allCount: allVoteLinksCount
        };
    }

    let isRunning = false;
    let currentIndex = 0;
    let voteLinks = [];
    let totalLinks = 0;
    let allVoteLinksCount = 0;
    let stats = {success: 0, failed: 0, skipped: 0};

    function startAutoVote() {
        if (isRunning) return;

        const {unvoted, skipped, allCount} = getVoteLinks();
        voteLinks = unvoted;
        totalLinks = voteLinks.length;
        allVoteLinksCount = allCount;
        stats.skipped = skipped;

        if (voteLinks.length === 0) {
            showDialog('当前页面没有可投票的项目!');
            updateProgress(0, 0, allVoteLinksCount);
            return;
        }

        isRunning = true;
        currentIndex = 0;
        stats.success = 0;
        stats.failed = 0;
        updateProgress(0, totalLinks, allVoteLinksCount);
        updateStats(0, 0, stats.skipped);
        clickNextLink();
    }

    let currentRequest = null;

    function clickNextLink() {
        if (!isRunning || currentIndex >= totalLinks) {
            isRunning = false;
            const actionButton = document.getElementById('voteActionButton');
            if (actionButton) {
                actionButton.textContent = '开始';
                actionButton.style.backgroundColor = '#4CAF50';
            }
            return;
        }

        updateProgress(currentIndex, totalLinks, allVoteLinksCount);
        const currentLink = voteLinks[currentIndex].href;
        const currentId = extractIdFromUrl(currentLink);
        updateCurrentId(currentId);

        const request = GM_xmlhttpRequest({
            method: "GET",
            url: currentLink,
            timeout: config.timeout,
            onload: function(response) {
                if (response.status >= 200 && response.status < 300) {
                    recordSuccessfulVote(currentLink);
                    stats.success++;
                } else {
                    stats.failed++;
                }

                updateStats(stats.success, stats.failed, stats.skipped);
                currentIndex++;
                setTimeout(clickNextLink, config.defaultDelay);
            },
            onerror: function(response) {
                stats.failed++;
                updateStats(stats.success, stats.failed, stats.skipped);
                currentIndex++;
                setTimeout(clickNextLink, config.defaultDelay);
            },
            ontimeout: function(response) {
                stats.failed++;
                updateStats(stats.success, stats.failed, stats.skipped);
                currentIndex++;
                setTimeout(clickNextLink, config.defaultDelay);
            }
        });
    }

    function stopAutoVote() {
        isRunning = false;
        if (currentRequest) {
            currentRequest.abort();
            currentRequest = null;
        }
    }

    // 在脚本开头加载配置,使用当前域名存储配置
    const savedConfig = GM_getValue(`config_${currentDomain}`);
    if (savedConfig) {
        Object.assign(config, savedConfig);
    }

    function showSettings() {
        const dialog = document.createElement('div');
        dialog.className = 'vote-dialog';
        // 应用透明度到设置面板
        const dialogBgColor = `rgba(68, 68, 68, ${config.panelOpacity})`;
        dialog.style.backgroundColor = dialogBgColor;

        dialog.innerHTML = `
            <div class="vote-dialog-title">参数设置</div>
            <div class="vote-dialog-content">
                <div style="margin-bottom:10px;">
                    <label style="display:block; margin-bottom:5px; font-size:12px; color:#ccc;">请求间隔(ms):</label>
                    <input type="number" id="delayInput" value="${config.defaultDelay}" style="width:100%; padding:5px; background:#555; color:#fff; border:1px solid #666;">
                </div>
                <div style="margin-bottom:15px;">
                    <label style="display:block; margin-bottom:5px; font-size:12px; color:#ccc;">超时时间(ms):</label>
                    <input type="number" id="timeoutInput" value="${config.timeout}" style="width:100%; padding:5px; background:#555; color:#fff; border:1px solid #666;">
                </div>
                <div style="margin-bottom:15px;">
                    <label style="display:block; margin-bottom:5px; font-size:12px; color:#ccc;">面板透明度(0-1):</label>
                    <input type="number" id="opacityInput" value="${config.panelOpacity}" step="0.1" min="0" max="1" style="width:100%; padding:5px; background:#555; color:#fff; border:1px solid #666;">
                </div>
            </div>
            <div class="button-container" style="display: flex; gap: ${config.buttonSpacing}; margin-bottom: ${config.buttonSpacing};">
                <button id="resetConfig" class="vote-dialog-button" style="flex: 1; padding: 4px 8px; background-color:#FF9800; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">重置配置</button>
                <button id="resetHistory" class="vote-dialog-button" style="flex: 1; padding: 4px 8px; background-color:#f44336; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">重置历史统计</button>
            </div>
            <div class="button-container" style="display: flex; gap: ${config.buttonSpacing};">
                <button id="saveSettings" class="vote-dialog-button" style="flex: 1; padding: 4px 8px; background-color:#4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">保存</button>
                <button id="cancelSettings" class="vote-dialog-button" style="flex: 1; padding: 4px 8px; background-color:#f44336; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">取消</button>
            </div>
        `;

        document.body.appendChild(dialog);

        document.getElementById('saveSettings').addEventListener('click', function() {
            config.defaultDelay = parseInt(document.getElementById('delayInput').value) || config.defaultDelay;
            config.timeout = parseInt(document.getElementById('timeoutInput').value) || config.timeout;
            config.panelOpacity = parseFloat(document.getElementById('opacityInput').value) || config.panelOpacity;
            GM_setValue(`config_${currentDomain}`, config);

            // 更新主面板透明度
            const panel = document.getElementById('autoVotePanel');
            if (panel) {
                panel.style.backgroundColor = `rgba(51, 51, 51, ${config.panelOpacity})`;
            }

            // 更新设置面板透明度
            dialog.style.backgroundColor = `rgba(68, 68, 68, ${config.panelOpacity})`;

            document.body.removeChild(dialog);
        });

        document.getElementById('resetConfig').addEventListener('click', function() {
            // 修复:默认配置包含透明度
            const defaultConfig = {
                defaultDelay: 1000,
                timeout: 2500,
                panelBgColor: '#333',
                buttonSpacing: '5px',
                panelOpacity: 0.9 // 默认透明度改为 90%
            };
            Object.assign(config, defaultConfig);
            GM_setValue(`config_${currentDomain}`, config);

            // 更新输入框值
            document.getElementById('delayInput').value = config.defaultDelay;
            document.getElementById('timeoutInput').value = config.timeout;
            document.getElementById('opacityInput').value = config.panelOpacity;  // 新增:更新透明度输入框

            // 新增:重置面板透明度
            const panel = document.getElementById('autoVotePanel');
            if (panel) {
                panel.style.backgroundColor = `rgba(51, 51, 51, ${config.panelOpacity})`;
            }
        });

        document.getElementById('resetHistory').addEventListener('click', function() {
            // 清除当前网站的投票历史记录
            const allHistory = GM_getValue('voteHistory', {});
            allHistory[currentDomain] = {};
            GM_setValue('voteHistory', allHistory);
            // 更新历史投票数显示
            updateHistoryCount();
        });

        document.getElementById('cancelSettings').addEventListener('click', function() {
            document.body.removeChild(dialog);
        });
    }

    createControlButton();
})();