GTA World Forums – Like All + Post Counter

One‑click “Like All” for the current GTA World forum page + live post counter.

// ==UserScript==
// @name         GTA World Forums – Like All + Post Counter
// @version      1.0
// @description  One‑click “Like All” for the current GTA World forum page + live post counter.
// @author       blanco
// @license      All Rights Reserved
// @match        https://forum.gta.world/*
// @grant        none
// @namespace https://gf.qytechs.cn/users/1496525
// ==/UserScript==

// © 2025 blanco. All rights reserved.
// This script is proprietary. You may not copy, modify, distribute, or use any part of it without explicit written permission.

(() => {
    'use strict';

    const CONFIG = {
        likeDelay: 500,
        badgeBottom: 150,
        counterDebounce: 250,
        hotkey: 'L',
        maxRetries: 3,
        rateLimitPause: 30000
    };

    const notifEl = (() => {
        const el = document.createElement('div');
        el.id = 'forum-like-notification';
        el.setAttribute('role', 'alert');
        Object.assign(el.style, {
            position: 'fixed', top: '20px', right: '20px',
            padding: '12px 24px', borderRadius: '6px', fontSize: '16px', zIndex: 10001,
            boxShadow: '0 2px 8px rgba(0,0,0,.18)', opacity: '.98', pointerEvents: 'none',
            transition: 'opacity .4s'
        });
        document.body.appendChild(el);
        return el;
    })();

    function showNotification(msg, dur = 3000) {
        const dark = matchMedia('(prefers-color-scheme: dark)').matches;
        notifEl.style.background = dark ? '#222' : '#f5f5f5';
        notifEl.style.color = dark ? '#fff' : '#222';
        notifEl.textContent = msg;
        notifEl.style.display = 'block';
        setTimeout(() => { notifEl.style.display = 'none'; }, dur);
    }

    const badgeEl = (() => {
        const el = document.createElement('div');
        el.id = 'forum-post-count';
        Object.assign(el.style, {
            position: 'fixed', right: '20px', bottom: `${CONFIG.badgeBottom}px`,
            padding: '8px 16px', borderRadius: '8px', fontSize: '15px', zIndex: 10001,
            boxShadow: '0 2px 8px rgba(0,0,0,.18)', pointerEvents: 'none', userSelect: 'none'
        });
        document.body.appendChild(el);
        return el;
    })();

    const debounce = (fn, wait) => {
        let t;
        return (...args) => {
            clearTimeout(t);
            t = setTimeout(() => fn.apply(null, args), wait);
        };
    };

    function updateBadge() {
        const cnt = document.querySelectorAll('article[id^="elComment_"]').length;
        const dark = matchMedia('(prefers-color-scheme: dark)').matches;
        badgeEl.style.background = dark ? '#222' : '#f5f5f5';
        badgeEl.style.color = dark ? '#fff' : '#222';
        badgeEl.textContent = `Posts: ${cnt}`;
    }

    const debouncedUpdate = debounce(updateBadge, CONFIG.counterDebounce);
    window.addEventListener('load', updateBadge);

    const ROOT = document.getElementById('elContent') || document.body;
    new MutationObserver(mutations => {
        for (const rec of mutations) {
            if (rec.addedNodes.length || rec.removedNodes.length) {
                debouncedUpdate();
                break;
            }
        }
    }).observe(ROOT, { childList: true, subtree: true });

    const sleep = ms => new Promise(r => setTimeout(r, ms));

    function fetchUpvoteLinks() {
        return Array.from(document.querySelectorAll('span.ipsReact_button[data-action="reactLaunch"]:not(.ipsReact_button_selected):not(.ipsReact_button--selected) a.ipsReact_reaction')).filter(a => a.querySelector('img[alt="Upvote"]'));
    }

    const abortCtrl = new AbortController();
    window.addEventListener('beforeunload', () => abortCtrl.abort());

    async function likeViaAjax(link, attempt = 0) {
        try {
            const r = await fetch(link.href, {
                method: 'GET', credentials: 'include', signal: abortCtrl.signal,
                headers: { 'X-Requested-With': 'XMLHttpRequest', 'Referer': location.href }
            });
            if (r.status === 429) {
                showNotification('Rate‑limit hit – pausing 30 s');
                await sleep(CONFIG.rateLimitPause);
                return likeViaAjax(link, attempt + 1);
            }
            if (!r.ok) throw new Error();
            return true;
        } catch {
            if (attempt < CONFIG.maxRetries) {
                const backoff = 2 ** attempt * 1000;
                await sleep(backoff);
                return likeViaAjax(link, attempt + 1);
            }
            return false;
        }
    }

    const likeBtnImg = (() => {
        const img = document.createElement('img');
        img.src = 'https://i.imgur.com/b7IZU6X.gif';
        img.width = 56;
        img.height = 56;
        img.alt = 'Like All';
        Object.assign(img.style, {
            position: 'fixed', right: '20px', bottom: '80px', zIndex: 10001,
            borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,.18)', padding: '2px', cursor: 'pointer',
            background: matchMedia('(prefers-color-scheme: dark)').matches ? '#222' : '#fff'
        });
        img.setAttribute('aria-label', 'Like all posts on this page');
        img.tabIndex = 0;
        document.body.appendChild(img);
        return img;
    })();

    function toggleBtn(disabled, label) {
        likeBtnImg.style.opacity = disabled ? '0.5' : '1';
        likeBtnImg.setAttribute('aria-label', label);
        likeBtnImg.title = label;
    }

    async function likeAll() {
        const links = fetchUpvoteLinks();
        if (!links.length) {
            showNotification('No un‑liked posts found on this page.');
            return;
        }
        toggleBtn(true, `Liking 0 / ${links.length}`);
        let ok = 0, fail = 0;
        for (let i = 0; i < links.length; i++) {
            toggleBtn(true, `Liking ${i + 1} / ${links.length}`);
            (await likeViaAjax(links[i])) ? ok++ : fail++;
            await sleep(CONFIG.likeDelay);
        }
        toggleBtn(false, 'Like all posts on this page');
        showNotification(`Finished! ✔️ ${ok} ❌ ${fail}`);
    }

    likeBtnImg.addEventListener('click', likeAll);
    likeBtnImg.addEventListener('keydown', e => {
        if (e.key === 'Enter' || e.key === ' ') likeAll();
    });

    window.addEventListener('keydown', e => {
        if (e.shiftKey && e.key.toUpperCase() === CONFIG.hotkey && !e.repeat) {
            e.preventDefault();
            likeAll();
        }
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址