Показывает статистику владельцев/желающих, цены на лоты и число обменов пользователей
// ==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()}();