您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a "renew all" button to renew all Craigslist postings that show a "renew" button.
// ==UserScript== // @name Craigslist – Renew All Postings // @namespace https://x.com/ArtemR // @version 1.0 // @description Adds a "renew all" button to renew all Craigslist postings that show a "renew" button. // @author Artem Russakovskii // @match https://accounts.craigslist.org/login/home* // @run-at document-idle // @license MIT // @grant none // ==/UserScript== (function () { 'use strict'; // ---- CONFIG ---- const DELAY_MS = 100; // Increase if you hit rate limits const $ = (sel, root=document) => root.querySelector(sel); const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel)); const sleep = (ms) => new Promise(res => setTimeout(res, ms)); function makeBadge(text) { const s = document.createElement('small'); s.textContent = text; s.style.marginLeft = '0.5em'; s.style.padding = '0.05em 0.35em'; s.style.borderRadius = '0.5em'; s.style.border = '1px solid #aaa'; s.style.fontSize = '0.9em'; s.style.background = '#ffffe0'; return s; } // --- Icon helpers --- function getRenewSubmit(form) { // Try to find the "renew" submit button within the renew form return $('input[type="submit"]', form) || $('button[type="submit"]', form); } function getOrCreateIcon(form) { let icon = $('.tm-renew-icon', form); if (!icon) { icon = document.createElement('span'); icon.className = 'tm-renew-icon'; const btn = getRenewSubmit(form); // Place immediately after the Renew button for best visibility if (btn) { icon.style.marginLeft = '0.33em'; icon.style.fontSize = '1.05em'; icon.style.verticalAlign = 'middle'; btn.insertAdjacentElement('afterend', icon); } else { // Fallback: append to form form.appendChild(icon); } } return icon; } function setIcon(form, state) { const icon = getOrCreateIcon(form); if (!icon) return; // Reset styles icon.style.opacity = '1'; icon.style.filter = ''; if (state === 'queued') { icon.textContent = '…'; icon.title = 'Queued'; icon.style.opacity = '0.6'; } else if (state === 'renewing') { icon.textContent = '⏳'; icon.title = 'Renewing…'; } else if (state === 'renewed') { icon.textContent = '✔️'; icon.title = 'Renewed'; } else if (state === 'failed') { icon.textContent = '❌'; icon.title = 'Failed'; } else { icon.textContent = ''; icon.title = ''; } } function serializeForm(form) { const params = new URLSearchParams(); $$('input, select, textarea', form).forEach(el => { if (!el.name || el.disabled) return; const type = (el.type || '').toLowerCase(); if ((type === 'checkbox' || type === 'radio') && !el.checked) return; params.append(el.name, el.value ?? ''); }); return params; } async function renewForm(form, row) { const url = form.getAttribute('action'); const body = serializeForm(form); try { const resp = await fetch(url, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(), redirect: 'follow', }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); markRow(row, 'renewed'); setIcon(form, 'renewed'); return true; } catch (e) { console.error('Renew failed:', e); markRow(row, 'failed'); setIcon(form, 'failed'); return false; } } function markRow(row, status) { row.style.transition = 'background-color .25s ease'; if (status === 'queued') { row.style.backgroundColor = '#fffbe6'; } else if (status === 'renewing') { row.style.backgroundColor = '#fff1b8'; } else if (status === 'renewed') { row.style.backgroundColor = '#f6ffed'; row.style.outline = '1px solid #b7eb8f'; } else if (status === 'failed') { row.style.backgroundColor = '#fff1f0'; row.style.outline = '1px solid #ffa39e'; } } function getRenewTargets() { const rows = $$('#paginator table.accthp_postings tbody tr.posting-row'); const targets = []; rows.forEach(row => { const form = $('form.manage.renew', row); if (form) { // Precreate a placeholder icon so you see progress immediately setIcon(form, 'queued'); targets.push({ form, row }); } }); return targets; } async function renewAll({ delayMs = DELAY_MS } = {}) { const targets = getRenewTargets(); if (!targets.length) { alert('No renew buttons found on this page.'); return; } const prog = $('#tm-renew-all-progress') || makeBadge(''); prog.id = 'tm-renew-all-progress'; let ok = 0, fail = 0; for (let i = 0; i < targets.length; i++) { const { form, row } = targets[i]; markRow(row, 'queued'); setIcon(form, 'renewing'); prog.textContent = `renewing ${i + 1}/${targets.length}…`; markRow(row, 'renewing'); const success = await renewForm(form, row); if (success) ok++; else fail++; prog.textContent = `done ${i + 1}/${targets.length} (✔︎ ${ok} · ✖︎ ${fail})`; if (i < targets.length - 1) await sleep(delayMs); } } function insertRenewAllLink() { const manageHeaderInner = $$('thead th .tablesorter-header-inner') .find(el => (el.textContent || '').trim().toLowerCase().startsWith('manage')); if (!manageHeaderInner || $('#tm-renew-all')) return; const sep = document.createElement('span'); sep.textContent = ' '; const link = document.createElement('a'); link.href = '#'; link.id = 'tm-renew-all'; link.textContent = 'renew all'; link.title = 'Submit all Renew forms on this page'; link.style.marginLeft = '0.5em'; link.style.fontSize = '0.9em'; link.style.textDecoration = 'underline'; link.addEventListener('click', (e) => { e.preventDefault(); link.style.pointerEvents = 'none'; link.style.opacity = '0.6'; const prog = $('#tm-renew-all-progress') || makeBadge(''); prog.id = 'tm-renew-all-progress'; manageHeaderInner.appendChild(prog); renewAll().finally(() => { link.style.pointerEvents = 'auto'; link.style.opacity = '1'; }); }); manageHeaderInner.appendChild(sep); manageHeaderInner.appendChild(link); } const tabHeader = $('.account-tab-header'); if (tabHeader && /postings/i.test(tabHeader.textContent || '')) { insertRenewAllLink(); } })();