您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Insert stats box right below profile tabs inside #bodyarea on Bitcointalk profile page
// ==UserScript== // @name Bitcointalk Monthly Stats // @namespace https://bitcointalk.org // @version 1.1 // @description Insert stats box right below profile tabs inside #bodyarea on Bitcointalk profile page // @author Ace // @match https://bitcointalk.org/index.php?action=profile* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; const boxId = 'monthlyStatsBox'; const now = new Date(); let currentMonthOffset = 0; function pad(n) { return n.toString().padStart(2, '0'); } function addOneDayWithRandomSeconds(dateString) { const d = new Date(dateString); d.setDate(d.getDate() + 1); const seconds = pad(Math.floor(Math.random() * 59) + 1); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00:${seconds}`; } function getDateRange(monthOffset = 0) { const date = new Date(now.getFullYear(), now.getMonth() + monthOffset, 1); const year = date.getFullYear(); const month = date.getMonth() + 1; const firstDay = `${year}-${pad(month)}-01`; const lastDay = new Date(year, month, 0).getDate(); const lastDate = `${year}-${pad(month)}-${pad(lastDay)}`; const label = `${date.toLocaleString('en', { month: 'long' })} ${year}`; return { from: firstDay, to: lastDate, label, y: year, m: month }; } function extractUidAndUsername() { const uidMatch = location.href.match(/u=(\d+)/); const uid = uidMatch ? uidMatch[1] : null; const nameRow = Array.from(document.querySelectorAll('td')).find(td => td.textContent.trim() === 'Name:'); const username = nameRow ? nameRow.nextElementSibling.textContent.trim() : null; return { uid, username }; } async function fetchBoardData(username, from, to) { const url = `https://api.ninjastic.space/users/${username}/boards?from=${from}T00:00:00&to=${addOneDayWithRandomSeconds(to)}`; const res = await fetch(url); const json = await res.json(); if (json.result !== 'success') return null; return json.data; } async function fetchMerit(type, username, y, m) { const from = `${y}-${pad(m)}-01`; const to = `${y}-${pad(m)}-${pad(new Date(y, m, 0).getDate())}`; const param = type === 'received' ? 'to' : 'from'; const url = `https://api.allorigins.win/raw?url=${encodeURIComponent(`https://bpip.org/smerit.aspx?&${param}=${username}&start=${from}&end=${to}`)}`; const res = await fetch(url); const htmlText = await res.text(); const doc = new DOMParser().parseFromString(htmlText, 'text/html'); const rows = Array.from(doc.querySelectorAll('table tbody tr')); if (!rows.length) return null; const data = {}; let total = 0; rows.forEach(tr => { const tds = tr.querySelectorAll('td'); if (tds.length >= 4) { let name = tds[type === 'received' ? 1 : 2].innerText.trim(); name = name.replace(/\s*\(Summary\)$/, ''); const count = parseInt(tds[3].innerText.trim()) || 0; total += count; data[name] = (data[name] || 0) + count; } }); return { total, data }; } function createBox() { let existing = document.getElementById(boxId); if (existing) return existing; const container = document.createElement('div'); container.id = boxId; container.style.background = '#222'; container.style.color = '#fff'; container.style.padding = '12px'; container.style.marginTop = '10px'; container.style.borderRadius = '12px'; container.style.fontSize = '13px'; container.style.maxWidth = '700px'; container.style.boxShadow = '0 0 8px rgba(0,0,0,0.6)'; container.style.fontFamily = 'Arial, sans-serif'; const toggleBtn = document.createElement('button'); toggleBtn.textContent = 'Hide Stats'; toggleBtn.style.marginBottom = '10px'; toggleBtn.style.padding = '5px 10px'; toggleBtn.style.border = 'none'; toggleBtn.style.borderRadius = '8px'; toggleBtn.style.background = '#555'; toggleBtn.style.color = '#fff'; toggleBtn.style.cursor = 'pointer'; toggleBtn.onclick = () => { if (container.style.display !== 'none') { container.style.display = 'none'; toggleBtn.textContent = 'Show Stats'; } else { container.style.display = 'block'; toggleBtn.textContent = 'Hide Stats'; } }; container.appendChild(toggleBtn); const statsContent = document.createElement('div'); statsContent.id = `${boxId}-content`; statsContent.innerHTML = 'Loading...'; container.appendChild(statsContent); const nav = document.createElement('div'); nav.style.marginTop = '8px'; nav.style.display = 'flex'; nav.style.justifyContent = 'space-between'; const prevBtn = document.createElement('button'); prevBtn.textContent = '← Previous Month'; prevBtn.style.flex = '1'; prevBtn.style.marginRight = '4px'; prevBtn.style.padding = '6px'; prevBtn.style.border = 'none'; prevBtn.style.borderRadius = '6px'; prevBtn.style.background = '#444'; prevBtn.style.color = '#fff'; prevBtn.style.cursor = 'pointer'; prevBtn.onclick = () => { currentMonthOffset--; renderStats(); }; const nextBtn = document.createElement('button'); nextBtn.textContent = 'Next Month →'; nextBtn.style.flex = '1'; nextBtn.style.marginLeft = '4px'; nextBtn.style.padding = '6px'; nextBtn.style.border = 'none'; nextBtn.style.borderRadius = '6px'; nextBtn.style.background = '#444'; nextBtn.style.color = '#fff'; nextBtn.style.cursor = 'pointer'; nextBtn.onclick = () => { if (currentMonthOffset < 0) { currentMonthOffset++; renderStats(); } }; nav.appendChild(prevBtn); nav.appendChild(nextBtn); container.appendChild(nav); const bodyarea = document.getElementById('bodyarea'); if (bodyarea) { bodyarea.insertBefore(container, bodyarea.firstChild); } else { // fallback document.body.insertBefore(container, document.body.firstChild); } return container; } async function renderStats() { const { uid, username } = extractUidAndUsername(); if (!uid || !username) return; const box = createBox(); const content = document.getElementById(`${boxId}-content`); content.innerHTML = '📊 Loading monthly data...'; const { from, to, label, y, m } = getDateRange(currentMonthOffset); const boardData = await fetchBoardData(username, from, to); const meritReceived = await fetchMerit('received', username, y, m); const meritSent = await fetchMerit('sent', username, y, m); if (!boardData) { content.innerHTML = '❌ Error loading posts.'; return; } let html = `🧮 <b>Statistics for ${label} – ${username}</b><br><br>`; html += `📝 <b>Posts written:</b> ${boardData.total_results_with_board}<br>`; boardData.boards.forEach(b => { html += `• ${b.name}: ${b.count}<br>`; }); if (!meritReceived) { html += `<br>⭐ <b>Merits received:</b> Loading error.`; } else { html += `<br>⭐ <b>Merits received:</b> ${meritReceived.total}<br>`; Object.entries(meritReceived.data).sort((a, b) => b[1] - a[1]).forEach(([name, count]) => { html += `• ${name}: ${count}<br>`; }); } if (!meritSent) { html += `<br>🎁 <b>Merits sent:</b> Loading error.`; } else { html += `<br>🎁 <b>Merits sent:</b> ${meritSent.total}<br>`; Object.entries(meritSent.data).sort((a, b) => b[1] - a[1]).forEach(([name, count]) => { html += `• ${name}: ${count}<br>`; }); } content.innerHTML = html; } if (location.href.includes('action=profile')) { renderStats(); } })();