MangaBuff Card Statistics

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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()}();