MangaBuff Card Statistics

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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