您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
One-click “Like All” (uses current/default reaction only) for the current GTA World forum topic page + live post counter.
当前为
// ==UserScript== // @name GTA World Forums – Like All (Default Reaction) + Post Counter // @version 1.1 // @description One-click “Like All” (uses current/default reaction only) for the current GTA World forum topic page + live post counter. // @author blanco // @license All Rights Reserved // @namespace https://gf.qytechs.cn/users/1496525 // @match https://forum.gta.world/*topic/* // @run-at document-idle // @noframes // @grant none // @icon data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M2 21h4V9H2v12zM22 10a2 2 0 0 0-2-2h-5.31l.95-4.57.03-.32a1 1 0 0 0-.29-.7L14 2 7.59 8.41A2 2 0 0 0 7 9.83V19a2 2 0 0 0 2 2h8a2 2 0 0 0 1.85-1.23l3-7a2 2 0 0 0 .15-.77v-2z'/></svg> // ==/UserScript== (() => { 'use strict'; const CONFIG = { likeDelay: 500, likeDelayJitterPct: 0.4, badgeBottom: 150, counterDebounce: 250, maxRetries: 3, rateLimitPause: 30000 }; const state = { running: false, paused: false, aborted: false }; const sleep = (ms) => new Promise(r => setTimeout(r, ms)); const jitter = (ms, pct=CONFIG.likeDelayJitterPct) => { const d = ms * pct; return Math.max(0, Math.round(ms + (Math.random() * 2 - 1) * d)); }; const whenVisible = async () => { while (document.visibilityState === 'hidden') { await sleep(500); } }; const parseRetryAfter = (v) => { if (!v) return null; const n = Number(v); if (!Number.isNaN(n)) return n * 1000; const t = Date.parse(v); const diff = t - Date.now(); return Number.isFinite(diff) && diff > 0 ? diff : null; }; const notifEl = (() => { const el = document.createElement('div'); el.id = 'forum-like-notification'; el.setAttribute('role', 'status'); el.setAttribute('aria-live', 'polite'); 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 .2s', maxWidth: '60ch' }); document.body.appendChild(el); return el; })(); let notifTimer = null; 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'; if (notifTimer) clearTimeout(notifTimer); notifTimer = setTimeout(() => { notifEl.style.display = 'none'; }, dur); } const badgeEl = (() => { const el = document.createElement('div'); el.id = 'forum-post-count'; el.setAttribute('aria-live', 'polite'); 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; })(); 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 debounce = (fn, wait) => { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn.apply(null, args), wait); }; }; const debouncedUpdate = debounce(() => { requestAnimationFrame(updateBadge); }, CONFIG.counterDebounce); window.addEventListener('load', updateBadge); const ROOT = document.getElementById('elContent') || document.body; const mo = new MutationObserver(muts => { for (const m of muts) { if (m.addedNodes.length || m.removedNodes.length) { debouncedUpdate(); break; } } }); mo.observe(ROOT, { childList: true, subtree: true }); const controlsWrap = (() => { const wrap = document.createElement('div'); Object.assign(wrap.style, { position: 'fixed', right: '20px', bottom: '80px', zIndex: 10001, display: 'flex', gap: '8px', alignItems: 'center' }); document.body.appendChild(wrap); return wrap; })(); function makeBtn(label, aria, onClick) { const b = document.createElement('button'); b.type = 'button'; b.textContent = label; b.setAttribute('aria-label', aria); Object.assign(b.style, { padding: '8px 10px', borderRadius: '10px', border: '1px solid rgba(0,0,0,.1)', boxShadow: '0 2px 8px rgba(0,0,0,.08)', cursor: 'pointer', background: matchMedia('(prefers-color-scheme: dark)').matches ? '#222' : '#fff', color: matchMedia('(prefers-color-scheme: dark)').matches ? '#fff' : '#222', fontSize: '13px' }); b.addEventListener('click', onClick); b.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') onClick(); }); controlsWrap.appendChild(b); return b; } const startBtn = makeBtn('Like All', 'Like all posts on this page', () => toggleRun()); const pauseBtn = makeBtn('Pause', 'Pause liking', () => togglePause()); const stopBtn = makeBtn('Stop', 'Stop liking', () => stopRun()); function reflectButtons() { pauseBtn.textContent = state.paused ? 'Resume' : 'Pause'; startBtn.disabled = state.running && !state.paused; pauseBtn.disabled = !state.running; stopBtn.disabled = !state.running; } function toggleBtnProgress(i, total) { const label = i == null ? 'Like All' : `Liking ${i} / ${total}`; startBtn.title = label; startBtn.setAttribute('aria-label', label); } function fetchDefaultReactionLinks() { const sel = 'span.ipsReact_button[data-action="reactLaunch"]:not(.ipsReact_button_selected):not(.ipsReact_button--selected) a.ipsReact_reaction[data-role="reaction"][data-defaultreaction]'; const seen = new Set(); const links = []; document.querySelectorAll(sel).forEach(a => { const href = a.getAttribute('href') || ''; if (href.includes('do=reactComment') && /[?&]reaction=\d+/.test(href) && /[?&]csrfKey=/.test(href)) { if (!seen.has(href)) { seen.add(href); links.push(a); } } }); return links; } 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) { const ra = parseRetryAfter(r.headers.get('Retry-After')); const pause = ra ?? CONFIG.rateLimitPause; showNotification(`Rate-limited — pausing ${Math.round(pause/1000)}s`); await sleep(pause); return likeViaAjax(link, attempt + 1); } if (!r.ok) throw new Error(`HTTP ${r.status}`); return true; } catch { if (attempt < CONFIG.maxRetries && !abortCtrl.signal.aborted) { const backoff = jitter(2 ** attempt * 1000, 0.25); await sleep(backoff); return likeViaAjax(link, attempt + 1); } return false; } } async function run() { state.running = true; state.paused = false; state.aborted = false; reflectButtons(); const links = fetchDefaultReactionLinks(); if (!links.length) { state.running = false; reflectButtons(); showNotification('No un-reacted posts found on this page.'); return; } let ok = 0, fail = 0; toggleBtnProgress(0, links.length); links.sort(() => Math.random() - 0.5); for (let i = 0; i < links.length; i++) { if (state.aborted || abortCtrl.signal.aborted) break; while (state.paused) await sleep(150); await whenVisible(); toggleBtnProgress(i + 1, links.length); (await likeViaAjax(links[i])) ? ok++ : fail++; await sleep(jitter(CONFIG.likeDelay)); } state.running = false; state.paused = false; reflectButtons(); toggleBtnProgress(null, null); if (!state.aborted) showNotification(`Finished! ✔️ ${ok} ❌ ${fail}`); } function toggleRun() { if (!state.running) run(); } function togglePause() { if (!state.running) return; state.paused = !state.paused; reflectButtons(); showNotification(state.paused ? 'Paused.' : 'Resumed.'); } function stopRun() { if (!state.running) return; state.aborted = true; state.paused = false; state.running = false; reflectButtons(); toggleBtnProgress(null, null); showNotification('Stopped.'); } window.addEventListener('beforeunload', () => { mo.disconnect(); if (notifTimer) clearTimeout(notifTimer); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址