MangaBuff Card Statistics

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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