MangaBuff Card Statistics

Показывает статистику владельцев/желающих, цены на лоты и число обменов пользователей

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         MangaBuff Card Statistics
// @namespace    http://tampermonkey.net/
// @version      2.0.3
// @description  Показывает статистику владельцев/желающих, цены на лоты и число обменов пользователей
// @author       zamoroz
// @match        https://mangabuff.ru/cards*
// @match        https://mangabuff.ru/users/*
// @match        https://mangabuff.ru/market*
// @match        https://mangabuff.ru/decks/*
// @match        https://mangabuff.ru/clubs/*/boost
// @match        https://mangabuff.ru/manga/*
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      mangabuff.ru
// @connect      mbstat.space
// @license MIT
// ==/UserScript==

!function(){"use strict";const t="https://mbstat.space",e=36e5,n=24,r=1,o=24,a="mangabuff_card_stats_cache",s="mangabuff_card_lots_cache",c="mangabuff_user_trades_cache",i={cards:{cardSelector:".manga-cards__item[data-card-id]",wrapperSelector:".manga-cards__item-wrapper",idAttribute:"data-card-id",idLocation:"card",showStats:!0,showLots:!1},deck:{cardSelector:".deck__item[data-card-id]",wrapperSelector:null,idAttribute:"data-card-id",idLocation:"card",showStats:!0,showLots:!1},market:{cardSelector:".market-list__cards--all .manga-cards__item",wrapperSelector:".manga-cards__item-wrapper",idAttribute:"data-id",idLocation:"wrapper",showStats:!1,showLots:!0},"club-boost":{cardSelector:".club-boost__inner",wrapperSelector:null,idAttribute:null,idLocation:"link",linkSelector:'a[href*="/cards/"]',showStats:!0,showLots:!1},manga:{cardSelector:".manga-cards__item[data-card-id], .lootbox__card[data-id]",wrapperSelector:".manga-cards__item-wrapper",idAttribute:"data-card-id,data-id",idLocation:"card",showStats:!0,showLots:!1}},l=document.createElement("style");l.textContent="\n        .card-stats-overlay {\n            background: rgba(0, 0, 0, 0.85);\n            color: white;\n            padding: 4px 6px;\n            border-radius: 3px;\n            font-size: 9px;\n            z-index: 10;\n            backdrop-filter: blur(5px);\n            line-height: 1.3;\n        }\n        .card-stats-row {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            margin: 1px 0;\n            white-space: nowrap;\n        }\n        .card-stats-label {\n            color: #aaa;\n            margin-right: 4px;\n            font-size: 8px;\n        }\n        .card-stats-value {\n            font-weight: bold;\n            font-size: 9px;\n        }\n        .card-stats-value.owners {\n            color: #4ade80;\n        }\n        .card-stats-value.wanters {\n            color: #fb923c;\n        }\n        .card-stats-value.card-stats-stale {\n            color: #9ca3af !important;\n        }\n        .card-stats-loading {\n            color: #888;\n            font-style: italic;\n        }\n        .manga-cards__item-wrapper {\n            position: relative;\n        }\n        .deck__item {\n            position: relative;\n        }\n        .club-boost__inner {\n            position: relative;\n        }\n        .lootbox__list {\n            padding-bottom: 40px;\n        }\n        .lootbox__card {\n            position: relative;\n        }\n        .card-lots-overlay {\n            position: absolute;\n            top: 8px;\n            right: 5px;\n            background: rgba(0, 0, 0, 0.85);\n            color: white;\n            padding: 4px 6px;\n            border-radius: 3px;\n            font-size: 8px;\n            z-index: 10;\n            backdrop-filter: blur(5px);\n            max-width: 110px;\n            line-height: 1.4;\n        }\n        .card-lots-label {\n            color: #aaa;\n            font-weight: 500;\n            margin-bottom: 2px;\n        }\n        .card-lots-prices {\n            color: #fbbf24;\n            font-weight: bold;\n        }\n        .profile__trades-count {\n            display: inline-block;\n            margin-left: 8px;\n            padding: 2px 8px;\n            background: rgba(139, 0, 255, 0.5);\n            border: 1px solid rgba(139, 0, 255, 0.8);\n            border-radius: 12px;\n            font-size: 12px;\n            color: #a78bfa;\n            font-weight: 500;\n            vertical-align: middle;\n        }\n        .profile__trades-blocked {\n            display: inline-block;\n            margin-left: 4px;\n            color: #ef4444;\n            font-size: 16px;\n            font-weight: bold;\n            vertical-align: middle;\n            cursor: help;\n        }\n    ",document.head.appendChild(l);const d=E(a,n),u=E(s,r),f=E(c,o),p=[];let m=!1,w=0;const h=[];let b=!1,g=0,_=!1;const y=0,S=1,v=[];let x=!1,k=null,q=null;function L(...t){console.log("[MangaBuff Stats]",...t)}function $(...t){console.error("[MangaBuff Stats]",...t)}function A(t){return new Promise(e=>setTimeout(e,t))}function C(t,n){return"number"==typeof t&&Date.now()-t<n*e}function E(t,n){try{const r=localStorage.getItem(t);if(!r)return{};const o=JSON.parse(r),a=Date.now();return Object.keys(o).forEach(t=>{const r=o[t];r?.timestamp&&a-r.timestamp>n*e&&delete o[t]}),o}catch(t){return $("Ошибка загрузки кеша:",t),{}}}function N(t,e){try{localStorage.setItem(t,JSON.stringify(e))}catch(t){$("Ошибка сохранения кеша:",t)}}function D(t,e,n,r=Date.now()){d[t]={owners:e,wanters:n,timestamp:r},N(a,d)}function P(){if(0===w)return w=Date.now(),Promise.resolve();const t=Date.now(),e=t-w;return e>=500?(w=t,Promise.resolve()):A(500-e).then(()=>{w=Date.now()})}function T(t){return new Promise((e,n)=>{p.push({requestFn:t,resolve:e,reject:n}),async function(){if(m||0===p.length)return;m=!0;for(;p.length>0;){const{requestFn:t,resolve:e,reject:n}=p.shift();try{e(await t())}catch(t){n(t)}}m=!1}()})}function O(){_||(_=!0,Promise.resolve().then(()=>{_=!1,async function(){if(b||0===h.length)return;b=!0;for(;h.length>0;){h.sort((t,e)=>t.priority-e.priority||t.order-e.order);const{requestFn:t,resolve:e,reject:n}=h.shift();try{e(await t())}catch(t){n(t)}}b=!1}()}))}async function j(t,e=0){return await P(),new Promise((n,r)=>{GM_xmlhttpRequest({method:"GET",url:t,onload:async function(o){if(429!==o.status)o.status>=200&&o.status<300?n(o.responseText):r(new Error(`HTTP ${o.status}`));else if(e<3){L(`429 ошибка для ${t}, повторная попытка ${e+1}/3`),await A(2e3*(e+1));try{const r=await j(t,e+1);n(r)}catch(t){r(t)}}else $(`Превышено количество попыток для ${t}`),r(new Error("Too many retries"))},onerror(t){r(t)}})})}function M(t){return(new DOMParser).parseFromString(t,"text/html")}function B(t){const e=M(t).querySelectorAll(".pagination__button a");let n=1;return e.forEach(t=>{const e=t.getAttribute("href");if(!e||!e.includes("page="))return;const r=e.match(/page=(\d+)/);if(!r)return;const o=parseInt(r[1],10);o>n&&(n=o)}),n}async function I(e){const n={};for(let r=0;r<e.length;r+=200){const o=e.slice(r,r+200),a=`${t}/cards?ids=${o.join(",")}`;await new Promise(t=>{GM_xmlhttpRequest({method:"GET",url:a,timeout:5e3,onload(e){if(e.status>=200&&e.status<300)try{JSON.parse(e.responseText).forEach(t=>{n[t.id]=t})}catch(t){$("Ошибка разбора ответа batch API:",t)}t()},onerror:t,ontimeout:t})})}return n}function z(e,n,r){const o=function(){try{const t=("undefined"!=typeof unsafeWindow?unsafeWindow:window).user_id;if(null==t||""===t)return null;const e=Number(t);return Number.isFinite(e)&&e>0?e:null}catch{return null}}();o&&null!==n&&null!==r&&GM_xmlhttpRequest({method:"POST",url:`${t}/cards/${e}`,headers:{"Content-Type":"application/json"},data:JSON.stringify({owners:Number(n),wanted:Number(r),user_id:o}),timeout:5e3,onload(){},onerror(){},ontimeout(){}})}async function F(t,e=S){return function(t,e=S){return new Promise((n,r)=>{h.push({requestFn:t,resolve:n,reject:r,priority:e,order:g++}),O()})}(async()=>{let e=null,n=null;try{await P();e=B(await j(`https://mangabuff.ru/cards/${t}/offers/want`))}catch{e=null}try{await P();n=B(await j(`https://mangabuff.ru/cards/${t}/users`))}catch{n=null}return null===n||null===e?null:{owners:n,wanted:e}},e)}function W(t,e,n={}){const{stale:r=!1}=n;if(Object.prototype.hasOwnProperty.call(e,"owners")){document.querySelectorAll(`[data-card-id="${t}"][data-type="owners"]`).forEach(t=>{t.textContent=null!==e.owners?e.owners:1,t.classList.remove("card-stats-loading"),t.classList.toggle("card-stats-stale",r)})}if(Object.prototype.hasOwnProperty.call(e,"wanters")){document.querySelectorAll(`[data-card-id="${t}"][data-type="wanters"]`).forEach(t=>{t.textContent=null!==e.wanters?e.wanters:1,t.classList.remove("card-stats-loading"),t.classList.toggle("card-stats-stale",r)})}}function G(t){const e=document.createElement("div");return e.className="card-stats-overlay",e.innerHTML=`\n            <div class="card-stats-row">\n                <span class="card-stats-label">Владельцев:</span>\n                <span class="card-stats-value owners card-stats-loading" data-card-id="${t}" data-type="owners">...</span>\n            </div>\n            <div class="card-stats-row">\n                <span class="card-stats-label">Желают:</span>\n                <span class="card-stats-value wanters card-stats-loading" data-card-id="${t}" data-type="wanters">...</span>\n            </div>\n        `,e}function J(t,e){if(e.wrapperSelector){const n=t.closest(e.wrapperSelector);if(n)return"static"===getComputedStyle(n).position&&(n.style.position="relative"),n}return"static"===getComputedStyle(t).position&&(t.style.position="relative"),t}function H(t,e){if("card"===e.idLocation){const n=e.idAttribute.split(",");for(const e of n){const n=t.getAttribute(e.trim());if(n)return n}return null}if("wrapper"===e.idLocation){const n=t.closest(e.wrapperSelector);return n?n.getAttribute(e.idAttribute):null}if("link"===e.idLocation){const n=t.querySelector(e.linkSelector);if(!n)return null;const r=n.getAttribute("href"),o=r?r.match(/\/cards\/(\d+)/):null;return o?o[1]:null}return null}async function R(t,e=y){const n=await F(t,e);n&&(W(t,{owners:n.owners,wanters:n.wanted}),D(t,n.owners,n.wanted),z(t,n.owners,n.wanted))}async function K(t){const e=u[t];return e&&C(e.timestamp,r)&&e.lots&&e.lots.length>0?e.lots:T(async()=>{try{const e=function(t){const e=M(t).querySelectorAll(".market-show__lots .market-show__item"),n=[],r=new Set;for(const t of e){const e=t.getAttribute("href"),o=e?e.split("/market/")[1]:null;if(!o)continue;const a=t.querySelector(".market-show__user-cards-rank"),s=a?a.textContent.trim():null;if(s&&!r.has(s)&&(r.add(s),n.push({lotId:o,price:s}),n.length>=5))break}return n}(await j(`https://mangabuff.ru/market/card/${t}`));return e.length>0&&(u[t]={lots:e,timestamp:Date.now()},N(s,u)),e}catch(e){return $(`Ошибка загрузки лотов для карты ${t}:`,e),[]}})}async function Q(t){const e=f[t];return e&&C(e.timestamp,o)&&null!==e.count?{count:e.count,isBlocked:e.isBlocked||!1}:T(async()=>{try{const e=function(t){const e=M(t),n=e.querySelector(".trade__header-name span");return{count:n?n.textContent.trim():null,isBlocked:null!==e.querySelector(".trade__block")}}(await j(`https://mangabuff.ru/trades/offers/${t}`));return null!==e.count&&(f[t]={count:e.count,isBlocked:e.isBlocked,timestamp:Date.now()},N(c,f)),e}catch{return{count:null,isBlocked:!1}}})}async function U(t,e){if(e.querySelector(".card-lots-overlay"))return;const n=await K(t);if(n&&n.length>0){const t=function(t){if(!t||0===t.length)return null;const e=document.createElement("div");return e.className="card-lots-overlay",e.innerHTML=`\n            <div class="card-lots-label">Цены:</div>\n            <div class="card-lots-prices">${t.map(t=>t.price).join(", ")}</div>\n        `,e}(n);t&&e.appendChild(t)}}function V(t,e){v.push({cardId:t,wrapper:e}),async function(){if(x||0===v.length)return;x=!0;for(;v.length>0;){const{cardId:t,wrapper:e}=v.shift();document.contains(e)&&!e.querySelector(".card-lots-overlay")&&await U(t,e)}x=!1}()}function X(t,e,n){const r=e.updated_at?Date.parse(e.updated_at):0,o=!r||n-r>3456e5;W(t,{owners:e.owners,wanters:e.wanted},{stale:o}),D(t,e.owners,e.wanted,n),o&&async function(t,e,n){const r=await F(t,S);r&&(D(t,r.owners,r.wanted),W(t,{owners:r.owners,wanters:r.wanted}),r.owners===e&&r.wanted===n||z(t,r.owners,r.wanted))}(t,e.owners,e.wanted)}function Y(t){R(t,y)}function Z(t,e){document.querySelector(".market-list__cards--all")&&(k||(k=new IntersectionObserver(t=>{t.forEach(t=>{if(!t.isIntersecting)return;const e=t.target,n=e.getAttribute("data-id");n&&(V(n,e),k.unobserve(e))})},{rootMargin:"50px"})),t.forEach(t=>{const n=t.closest(e.wrapperSelector);n&&k&&k.observe(n)}))}function tt(t,e){const r=[];t.forEach(t=>{if(e.showStats){const o=J(t,e);if(!o)return;if(o.querySelector(".card-stats-overlay"))return;const a=H(t,e);if(!a)return;const s=G(a);o.appendChild(s);const c=function(t){const e=d[t];return e&&C(e.timestamp,n)&&null!==e.owners&&null!==e.wanters?e:null}(a);c?W(a,{owners:c.owners,wanters:c.wanters}):r.push(a)}if(e.showLots){const n=J(t,e);if(!n)return;const r=H(t,e);if(!r)return;U(r,n)}}),r.length>0&&I(r).then(t=>{const e=Date.now();r.forEach(n=>{const r=t[n];void 0!==r?X(n,r,e):Y(n)})}).catch(t=>{$("Backend API error:",t),r.forEach(t=>Y(t))})}function et(){const t=Date.now(),e=function(){const t=window.location.pathname;return t.startsWith("/market")&!t.includes("requests")?"market":t.startsWith("/decks/")?"deck":t.match(/\/clubs\/[^/]+\/boost/)?"club-boost":t.startsWith("/cards")||t.match(/\/users\/\d+\/cards/)?"cards":t.startsWith("/manga/")?"manga":t.startsWith("/users/")?"profile":"unknown"}();if(L(`Начало processCards, тип страницы: ${e}`),"profile"===e||"unknown"===e)return void L(`Пропуск обработки для типа: ${e}`);const n=function(t){return i[t]||null}(e);if(!n)return void L(`Конфигурация не найдена для типа: ${e}`);const r=document.querySelectorAll(n.cardSelector);if(L(`Найдено ${r.length} карточек на странице ${e}`),0!==r.length){if("market"===e)return Z(r,n),void L(`processCards завершен за ${Date.now()-t}мс`);tt(r,n),L(`processCards завершен за ${Date.now()-t}мс`)}}function nt(t){return t&&1===t.nodeType}function rt(t){return t.classList?.contains("tabs__page")||t.querySelector?.(".manga-cards__item")||t.classList?.contains("lootbox__card")||t.querySelector?.(".lootbox__card")||t.classList?.contains("lootbox__list")}function ot(t){return t.classList?.contains("market-list__cards--all")||t.querySelector?.(".market-list__cards--all")}function at(t){return t.classList?.contains("lootbox__card")||t.querySelector?.(".lootbox__card")}function st(t){const e=t.querySelector(".card-stats-overlay");e&&e.remove();const n=t.getAttribute("data-id");if(!n)return;"static"===getComputedStyle(t).position&&(t.style.position="relative");const r=G(n);t.appendChild(r),I([n]).then(t=>{const e=t[n];if(void 0!==e){const t=Date.now();X(n,e,t)}else Y(n)})}const ct=new MutationObserver(t=>{let e=!1,n=!1;for(const r of t){if(r.addedNodes.length>0){const t=Array.from(r.addedNodes).filter(nt),n=t.some(rt),o=t.some(ot);(n||o)&&(e=!0)}if(r.removedNodes.length>0){Array.from(r.removedNodes).filter(nt).some(at)&&(e=!0)}if("attributes"===r.type&&"data-id"===r.attributeName){const t=r.target;t.classList.contains("lootbox__card")&&(n=!0,st(t))}}e?function(t=100){q&&clearTimeout(q),q=setTimeout(()=>{q=null,et()},t)}(100):n||et()});function it(){et(),async function(){const t=document.querySelector(".profile[data-user-id]");if(!t)return;const e=t.getAttribute("data-user-id");if(!e)return;const n=document.querySelector(".profile__name")||document.querySelector(".mobile-profile__name");if(!n)return;if(n.querySelector(".profile__trades-count"))return;const r=await Q(e);if(r&&null!==r.count){const t=document.createElement("span");t.className="profile__trades-count",t.textContent=r.count,t.title="Количество обменов",n.appendChild(t);const e=document.querySelector(".profile__info--ignore-btn"),o=e&&e.textContent.includes("Удалить из черного списка");if(r.isBlocked||o){const t=document.createElement("span");t.className="profile__trades-blocked",t.textContent="✖",n.appendChild(t)}}}(),ct.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["data-id"]})}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",it):it()}();