// ==UserScript==
// @name Card Helper AStars | AnimeStars | ASStars
// @namespace animestars.org
// @version 7.18
// @description Отображения спроса карт и Авто-Лут карточек с просмотра
// @author bmr
// @match https://astars.club/*
// @match https://asstars.club/*
// @match https://asstars1.astars.club/*
// @match https://animestars.org/*
// @match https://as1.astars.club/*
// @match https://asstars.tv/*
// @license MIT
// @grant none
// ==/UserScript==
const DELAY = 900;
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
let cardCounter = 0;
const cardClasses = '.remelt__inventory-item, .lootbox__card, .anime-cards__item, .trade__inventory-item, .trade__main-item, .card-filter-list__card, .deck__item, .history__body-item, .history__body-item, .card-pack__card';
let currentNotification = {
element: null,
id: null,
type: null,
timeoutId: null
};
function displayNotification(id, message, type = 'temporary', options = {}) {
if (window.location.pathname.includes('/pm/') || window.location.pathname.includes('emotions.php') || window.location.pathname.includes('/messages/')) {
return;
}
const { total, current, isSuccess = true, duration = 3500, sticky = false } = options;
if (currentNotification.element && currentNotification.id !== id) {
if (currentNotification.timeoutId) clearTimeout(currentNotification.timeoutId);
if (currentNotification.element.parentNode) {
currentNotification.element.remove();
}
currentNotification.element = null;
currentNotification.id = null;
}
let notificationElement = currentNotification.element;
if (!notificationElement || currentNotification.id !== id || (currentNotification.type === 'progress' && type !== 'progress')) {
if (notificationElement && notificationElement.parentNode) {
notificationElement.remove();
}
notificationElement = document.createElement('div');
notificationElement.className = 'card-helper-status-notification';
document.body.appendChild(notificationElement);
currentNotification.element = notificationElement;
currentNotification.id = id;
}
currentNotification.type = type;
let iconHtml = '';
if (type === 'progress') {
iconHtml = '<div class="card-helper-spinner"></div>';
if (total !== undefined && current !== undefined) {
let countText = total === 'неизвестно' ? `${current}` : `${current}/${total}`;
let progressMessageSuffix = `Обработано ${countText}`;
message = `${message} ${progressMessageSuffix}`;
}
} else if (type === 'completion' || type === 'temporary') {
const iconClass = isSuccess ?
'card-helper-checkmark' : 'card-helper-crossmark';
const iconChar = isSuccess ? '✔' : '✖';
iconHtml = `<span class="${iconClass}">${iconChar}</span>`;
}
notificationElement.innerHTML = `
<div class="ch-status-icon-container">${iconHtml}</div>
<span class="card-helper-status-text">${message}</span>
`;
requestAnimationFrame(() => {
notificationElement.classList.add('show');
});
if (currentNotification.timeoutId) {
clearTimeout(currentNotification.timeoutId);
currentNotification.timeoutId = null;
}
if (!sticky && (type === 'completion' || type === 'temporary')) {
currentNotification.timeoutId = setTimeout(() => {
hideCurrentNotification(id);
}, duration);
}
}
function updateNotificationProgress(id, messagePrefix, current, total) {
if (currentNotification.id === id && currentNotification.type === 'progress') {
const textElement = currentNotification.element.querySelector('.card-helper-status-text');
let countText = total === 'неизвестно' ? `${current}` : `${current}/${total}`;
let progressMessageSuffix = `Обработано ${countText}`;
const fullMessage = `${messagePrefix} ${progressMessageSuffix}`;
if (textElement && textElement.textContent !== fullMessage) {
textElement.textContent = fullMessage;
}
} else {
displayNotification(id, messagePrefix, 'progress', { current, total, sticky: true });
}
}
function completeProgressNotification(id, message, isSuccess = true, duration = 3500) {
displayNotification(id, message, 'completion', { isSuccess, duration });
}
function showTemporaryMessage(id, message, isSuccess = true, duration = 3500) {
displayNotification(id, message, 'temporary', { isSuccess, duration });
}
function hideCurrentNotification(idToHide) {
if (currentNotification.element && currentNotification.id === idToHide) {
const element = currentNotification.element;
element.classList.remove('show');
if (currentNotification.timeoutId) {
clearTimeout(currentNotification.timeoutId);
currentNotification.timeoutId = null;
}
setTimeout(() => {
if (element.parentNode) {
element.remove();
}
if (currentNotification.element === element) {
currentNotification.element = null;
currentNotification.id = null;
currentNotification.type = null;
}
}, 400);
}
}
function getCurrentDomain() {
const hostname = window.location.hostname;
const protocol = window.location.protocol;
return `${protocol}//${hostname}`;
}
async function loadCard(cardId, maxRetries = 2, initialRetryDelay = 2500) {
const cacheKey = 'cardId: ' + cardId;
const cachedCard = await getCard(cacheKey);
if (cachedCard) {
return cachedCard;
}
const currentDomain = getCurrentDomain();
let popularityCount = 0, needCount = 0, tradeCount = 0;
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
if (attempt > 1) {
const retryDelay = initialRetryDelay * Math.pow(1.5, attempt - 2);
console.warn(`Карта ${cardId}: Попытка ${attempt}/${maxRetries + 1}. Ждем ${retryDelay / 1000}с перед повтором...`);
await sleep(retryDelay);
} else {
await sleep(DELAY);
}
try {
const mainCardPageResponse = await fetch(`${currentDomain}/cards/users/?id=${cardId}`);
if (mainCardPageResponse.status === 403) {
console.error(`Карта ${cardId}: Попытка ${attempt} - Ошибка 403 Forbidden.`);
if (attempt === maxRetries + 1) return null;
continue;
}
if (!mainCardPageResponse.ok) {
console.error(`Карта ${cardId}: Попытка ${attempt} - Ошибка HTTP ${mainCardPageResponse.status}.`);
if (attempt === maxRetries + 1) return null;
continue;
}
const mainCardPageHtml = await mainCardPageResponse.text();
const mainCardPageDoc = new DOMParser().parseFromString(mainCardPageHtml, 'text/html');
popularityCount = parseInt(mainCardPageDoc.querySelector('#owners-count')?.textContent.trim(), 10) || 0;
needCount = parseInt(mainCardPageDoc.querySelector('#owners-need')?.textContent.trim(), 10) || 0;
tradeCount = parseInt(mainCardPageDoc.querySelector('#owners-trade')?.textContent.trim(), 10) || 0;
if (popularityCount === 0 && needCount === 0 && tradeCount === 0) {
if (attempt < maxRetries + 1) {
console.warn(`Карта ${cardId}: Попытка ${attempt} - все счетчики 0, повторяем.`);
continue;
}
console.warn(`Карта ${cardId}: Попытка ${attempt} (последняя) - принимаем нулевые счетчики.`);
}
const finalCardData = { popularityCount, needCount, tradeCount };
await cacheCard(cacheKey, finalCardData);
return finalCardData;
} catch (error) {
console.error(`Карта ${cardId}: Попытка ${attempt} - Исключение при загрузке:`, error);
if (attempt === maxRetries + 1) return null;
}
}
console.error(`Карта ${cardId}: Все ${maxRetries + 1} попытки загрузки не удались.`);
return null;
}
function extractOwnerIdFromUrl(url) {
const match = url.match(/\/user\/([^\/?#]+)/);
return match && match[1] ? match[1] : null;
}
async function updateCardInfo(cardId, element) {
if (!cardId || !element) return;
try {
const cardData = await loadCard(cardId);
const oldStats = element.querySelector('.card-stats');
if (oldStats) {
oldStats.remove();
}
if (!cardData) {
console.warn(`Не удалось загрузить данные для карты ${cardId}, информация не будет отображена.`);
return;
}
const stats = document.createElement('div');
stats.className = 'card-stats';
const currentMode = getCardStatsMode();
stats.classList.add(currentMode === 'full' ? 'card-stats--full' : 'card-stats--minimalistic');
if (currentMode === 'full') {
stats.innerHTML = `
<div class="stat-line"><i class="fas fa-users"></i> Имеют ${cardData.popularityCount}</div>
<div class="stat-line"><i class="fas fa-heart"></i> Хотят ${cardData.needCount}</div>
<div class="stat-line"><i class="fas fa-sync-alt"></i> Обмен ${cardData.tradeCount}</div>
`;
} else {
stats.innerHTML = `
<span title="Владельцев"><i class="fas fa-users"></i> ${cardData.popularityCount}</span>
<span title="Хотят получить"><i class="fas fa-heart"></i> ${cardData.needCount}</span>
<span title="Готовы обменять"><i class="fas fa-sync-alt"></i> ${cardData.tradeCount}</span>
`;
}
element.appendChild(stats);
} catch (error) {
console.error("Критическая ошибка в updateCardInfo для cardId " + cardId + ":", error);
}
}
function clearMarkFromCards() { cleanByClass('div-marked'); }
function removeAllLinkIcons() { cleanByClass('link-icon'); }
function cleanByClass(className) { document.querySelectorAll('.' + className).forEach(item => item.remove()); }
function getCardsOnPage() {
return Array.from(document.querySelectorAll(cardClasses)).filter(cardEl => cardEl.offsetParent !== null);
}
async function processCards() {
if (isCardRemeltPage()) {
const storedData = JSON.parse(localStorage.getItem('animeCardsData')) || {};
if (Object.keys(storedData).length < 1) { await readyRemeltCards(); return; }
}
removeMatchingWatchlistItems(); removeAllLinkIcons(); clearMarkFromCards();
const cardsOnPage = getCardsOnPage();
let totalCardsToProcess = cardsOnPage.length;
if (!totalCardsToProcess) return;
const buttonId = 'processCards';
startAnimation(buttonId);
displayNotification(buttonId, 'Проверка спроса:', 'progress', { current: 0, total: totalCardsToProcess, sticky: true });
let processedCardCount = 0;
for (const cardElement of cardsOnPage) {
if (cardElement.classList.contains('trade__inventory-item--lock') || cardElement.classList.contains('remelt__inventory-item--lock')) continue;
cardElement.classList.add('processing-card');
let cardId = await getCardId(cardElement);
if (cardId) {
await updateCardInfo(cardId, cardElement);
}
processedCardCount++;
updateNotificationProgress(buttonId, 'Проверено карт:', processedCardCount, totalCardsToProcess);
cardElement.classList.remove('processing-card');
if (cardElement.classList.contains('lootbox__card')) cardElement.addEventListener('click', removeAllLinkIcons);
}
completeProgressNotification(buttonId, 'Проверка спроса завершена', true);
stopAnimation(buttonId);
}
function getCardStatsMode() {
return localStorage.getItem('cardStatsMode') || 'minimalistic';
}
function setCardStatsMode(mode) {
localStorage.setItem('cardStatsMode', mode);
}
function removeMatchingWatchlistItems() {
const watchlistItems = document.querySelectorAll('.watchlist__item');
if (watchlistItems.length == 0) return;
let initialCount = watchlistItems.length;
watchlistItems.forEach(item => {
const episodesText = item.querySelector('.watchlist__episodes')?.textContent.trim();
if (episodesText) {
const matches = episodesText.match(/[\d]+/g);
if (matches) {
const currentEpisode = parseInt(matches[0], 10);
const totalEpisodes = parseInt(matches.length === 4 ? matches[3] : matches[1], 10);
if (currentEpisode === totalEpisodes) item.remove();
}
}
});
let currentCount = document.querySelectorAll('.watchlist__item').length;
if (initialCount > currentCount) {
showTemporaryMessage('watchlistUpdate', `Из списка удалены просмотренные аниме. Осталось: ${currentCount}`, true);
}
}
function startAnimation(id) {
const buttonElement = document.getElementById(id);
if (buttonElement) {
buttonElement.classList.add('is-working');
buttonElement.style.animationPlayState = 'paused';
const iconElement = buttonElement.querySelector('span[class*="fa-"]');
if (iconElement) {
iconElement.style.animation = 'pulseIcon 1s ease-in-out infinite';
}
}
}
function stopAnimation(id) {
const buttonElement = document.getElementById(id);
if (buttonElement) {
buttonElement.classList.remove('is-working');
if (!buttonElement.matches(':hover')) {
buttonElement.style.animationPlayState = 'running';
}
const iconElement = buttonElement.querySelector('span[class*="fa-"]');
if (iconElement) {
iconElement.style.animation = '';
}
}
}
function getButton(id, iconClassFASuffix, percent, tooltipText, clickFunction) {
const wrapper = document.createElement('div');
wrapper.style.position = 'fixed';
wrapper.style.top = percent + '%';
wrapper.style.right = '1%';
wrapper.style.zIndex = '1000';
const buttonElement = document.createElement('button');
buttonElement.id = id;
buttonElement.classList.add('anim-interactive-button');
const icon = document.createElement('span');
icon.className = 'fal fa-' + iconClassFASuffix;
buttonElement.appendChild(icon);
let tooltipTimeout;
const tooltip = document.createElement('div');
tooltip.className = 'anim-button-tooltip';
tooltip.textContent = tooltipText;
buttonElement.addEventListener('mouseenter', () => {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(-50%) translateX(0px)';
buttonElement.style.animationPlayState = 'paused';
if (tooltipTimeout) clearTimeout(tooltipTimeout);
});
buttonElement.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-50%) translateX(10px)';
if (!buttonElement.classList.contains('is-working')) {
buttonElement.style.animationPlayState = 'running';
}
});
buttonElement.addEventListener('click', (e) => {
e.stopPropagation();
clickFunction(e);
if (window.innerWidth <= 768) {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(-50%) translateX(0px)';
if (tooltipTimeout) clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(() => {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-50%) translateX(10px)';
}, 1500);
}
});
wrapper.appendChild(buttonElement);
wrapper.appendChild(tooltip);
return wrapper;
}
function createToggleStatsModeButton(topPercent) {
const wrapper = document.createElement('div');
wrapper.style.position = 'fixed';
wrapper.style.top = topPercent + '%';
wrapper.style.right = '1%';
wrapper.style.zIndex = '9998';
const button = document.createElement('button');
button.id = 'toggleStatsModeButton';
button.className = 'anim-interactive-button anim-interactive-button--small-toggle';
const icon = document.createElement('span');
function updateButtonAppearance() {
const currentMode = getCardStatsMode();
if (currentMode === 'minimalistic') {
icon.className = 'fal fa-ellipsis-h';
} else {
icon.className = 'fal fa-list-alt';
}
}
button.appendChild(icon);
updateButtonAppearance();
const tooltip = document.createElement('div');
tooltip.className = 'anim-button-tooltip';
tooltip.textContent = "Перекл. режимы отображения спроса";
let tooltipTimeout;
button.addEventListener('click', (e) => {
e.stopPropagation();
const oldMode = getCardStatsMode();
const newMode = oldMode === 'minimalistic' ? 'full' : 'minimalistic';
setCardStatsMode(newMode);
updateButtonAppearance();
const modeName = newMode === 'full' ? 'Полный' : 'Мин';
showTemporaryMessage('modeSwitched', `Режим статистики изменен на: ${modeName}.`, true, 4000);
if (window.innerWidth <= 768) {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(-50%) translateX(0px)';
if (tooltipTimeout) clearTimeout(tooltipTimeout);
tooltipTimeout = setTimeout(() => {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-50%) translateX(10px)';
}, 1500);
}
});
button.addEventListener('mouseenter', () => {
tooltip.style.opacity = '1';
tooltip.style.transform = 'translateY(-50%) translateX(0px)';
button.style.animationPlayState = 'paused';
if (tooltipTimeout) clearTimeout(tooltipTimeout);
});
button.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
tooltip.style.transform = 'translateY(-50%) translateX(10px)';
if (!button.classList.contains('is-working')) {
button.style.animationPlayState = 'running';
}
});
wrapper.appendChild(button);
wrapper.appendChild(tooltip);
return wrapper;
}
function addUpdateButton() {
if (window.location.pathname.includes('/pm/') || window.location.pathname.includes('emotions.php') || window.frameElement) return;
if (!document.getElementById('processCards')) {
document.body.appendChild(getButton('processCards', 'star', 42, 'Узнать спрос', processCards));
}
if (isMyCardPage() && !document.getElementById('readyToCharge')) {
document.body.appendChild(getButton('readyToCharge', 'handshake', 50, 'Отметить всё как "Готов обменять"', readyToCharge));
}
if (isCardRemeltPage() && !document.getElementById('readyRemeltCards')) {
document.body.appendChild(getButton('readyRemeltCards', 'yin-yang', 50, 'Кешировать карточки', readyRemeltCards));
}
if (!document.getElementById('clearCacheButton')) {
document.body.appendChild(getButton('clearCacheButton', 'trash', 58, 'Очистить кеш карт', clearCardCache));
}
if (!document.getElementById('promoCodeLinkButton')) {
document.body.appendChild(createPromoCodeButton());
}
if (!document.getElementById('toggleStatsModeButton')) {
document.body.appendChild(createToggleStatsModeButton(66));
}
}
function isMyCardPage() {
const pathname = window.location.pathname;
const search = window.location.search;
const oldPattern = /^\/user\/[^\/]+\/cards(\/page\/\d+\/)?$/;
if (oldPattern.test(pathname)) {
return true;
}
if (pathname === '/user/cards/' && search.startsWith('?name=')) {
const params = new URLSearchParams(search);
if (params.get('name') && params.get('name').length > 0) {
return true;
}
}
return false;
}
function isCardRemeltPage() { return (/^\/cards_remelt\//).test(window.location.pathname); }
async function readyRemeltCards() {
const buttonId = 'readyRemeltCards';
const notificationId = 'remeltCache';
showTemporaryMessage(notificationId, 'Запрос на кеширование всех карт..', true, 2000);
const userCardsLinkElement = document.querySelector('a.ncard__tabs-btn[href*="/user/"][href*="/cards/"]');
const relativeHref = userCardsLinkElement ? userCardsLinkElement.href : null;
if (!relativeHref) {
showTemporaryMessage(notificationId, 'Не найдена ссылка на страницу "Мои карты" для начала кеширования.', false, 5000);
return;
}
const absoluteHref = new URL(relativeHref, window.location.origin).href;
removeMatchingWatchlistItems();
removeAllLinkIcons();
clearMarkFromCards();
startAnimation(buttonId);
try {
await scrapeAllPages(absoluteHref);
} catch (e) {
showTemporaryMessage(notificationId, 'Произошла ошибка при кешировании.', false, 5000);
} finally {
stopAnimation(buttonId);
}
}
function getCanonicalIdFromCacheByItemInstanceId(itemInstanceIdToFind) {
const storedData = JSON.parse(localStorage.getItem('animeCardsData')) || {};
if (!itemInstanceIdToFind) return null;
for (const ownerKey in storedData) {
if (Array.isArray(storedData[ownerKey])) {
const foundCardData = storedData[ownerKey].find(card => card.itemInstanceId === itemInstanceIdToFind);
if (foundCardData && foundCardData.canonicalCardId) {
return foundCardData.canonicalCardId;
}
}
}
console.warn(`Канонический ID для экземпляра ${itemInstanceIdToFind} не найден в кеше animeCardsData.`);
return null;
}
async function scrapeAllPages(firstPageHref) {
const notificationId = 'scrapeAllPages';
try {
const response = await fetch(firstPageHref);
if (!response.ok) throw new Error(`Ошибка HTTP: ${response.status}`);
const firstPageHtml = await response.text();
const firstPageDoc = new DOMParser().parseFromString(firstPageHtml, 'text/html');
const pagination = firstPageDoc.querySelector('#pagination');
let storedData = JSON.parse(localStorage.getItem('animeCardsData')) || {};
let totalCardsInProfile = -1;
const titleElement = firstPageDoc.querySelector('h1.ncard__main-title.ncard__main-title-2.as-center.bolder');
if (titleElement) {
const match = titleElement.textContent.match(/\((\d+)\s*шт\.\)/);
if (match && match[1]) {
totalCardsInProfile = parseInt(match[1], 10);
}
}
const countCurrentlyCachedCards = () => {
let count = 0;
for (const ownerKey in storedData) {
if (Array.isArray(storedData[ownerKey])) {
count += storedData[ownerKey].length;
}
}
return count;
};
if (totalCardsInProfile !== -1) {
let currentTotalCached = countCurrentlyCachedCards();
if (totalCardsInProfile === currentTotalCached && currentTotalCached > 0) {
showTemporaryMessage(notificationId, 'Кеш карточек уже актуален.', true);
await processCards();
return;
}
}
const calculatedTotalForDisplay = totalCardsInProfile > 0 ? totalCardsInProfile : 'неизвестно';
displayNotification(notificationId, 'Кеширование страниц:', 'progress', { current: countCurrentlyCachedCards(), total: calculatedTotalForDisplay, sticky: true });
const currentAccountOwnerIdForCacheKey = extractOwnerIdFromUrl(firstPageHref);
async function processBatchInStorage(doc) {
const cardsElements = doc.querySelectorAll('.anime-cards__item');
let newCardsThisBatch = 0;
for (let i = 0; i < cardsElements.length; i += 10) {
const cardGroup = Array.from(cardsElements).slice(i, i + 10);
for (const cardEl of cardGroup) {
const itemInstanceId = cardEl.getAttribute('data-owner-id');
const canonicalCardId = cardEl.getAttribute('data-id');
if (!currentAccountOwnerIdForCacheKey || !itemInstanceId || !canonicalCardId) {
console.warn("Пропуск карты в scrapeAllPages: отсутствует itemInstanceId, canonicalCardId или ID владельца коллекции.", cardEl, "AccountOwnerID:", currentAccountOwnerIdForCacheKey);
continue;
}
const ownerKey = 'o_' + currentAccountOwnerIdForCacheKey;
if (!storedData[ownerKey]) storedData[ownerKey] = [];
if (!storedData[ownerKey].find(c => c.itemInstanceId === itemInstanceId)) {
storedData[ownerKey].push({
itemInstanceId: itemInstanceId,
canonicalCardId: canonicalCardId,
name: cardEl.getAttribute('data-name'),
rank: cardEl.getAttribute('data-rank'),
animeLink: cardEl.getAttribute('data-anime-link'),
image: cardEl.querySelector('img')?.getAttribute('src') || cardEl.getAttribute('data-image'),
ownerId: currentAccountOwnerIdForCacheKey
});
newCardsThisBatch++;
}
}
await sleep(10);
}
updateNotificationProgress(notificationId, 'Кешировано карт:', countCurrentlyCachedCards(), calculatedTotalForDisplay);
}
await processBatchInStorage(firstPageDoc);
if (pagination) {
const pageLinks = Array.from(pagination.querySelectorAll('a[href*="page/"]'));
let lastPageNumber = 1;
if (pageLinks.length > 0) {
const numbers = pageLinks.map(a => (a.getAttribute('href')?.match(/page\/(\d+)/) || [])[1]).map(n => parseInt(n, 10)).filter(n => n > 0);
if (numbers.length > 0) {
lastPageNumber = Math.max(...numbers);
} else {
const lastLinkElement = pagination.querySelector('a:last-of-type:not(.pagination__item--next)');
if(lastLinkElement) lastPageNumber = parseInt(lastLinkElement.textContent.trim(), 10) || 1;
}
}
if (lastPageNumber > 1) {
const parser = new DOMParser();
let basePageUrl = firstPageHref.replace(/\/page\/\d+(\/)?$/, '').replace(/\/$/, '');
for (let i = 2; i <= lastPageNumber; i++) {
const pageUrl = `${basePageUrl}/page/${i}/`;
const pageHTML = await (async (url) => {
try {
const r = await fetch(url);
return r.ok ? await r.text() : null;
} catch (e) {
console.error(`Ошибка при загрузке страницы ${url}:`, e);
return null;
}
})(pageUrl);
if (pageHTML) {
const nextPageDoc = parser.parseFromString(pageHTML, 'text/html');
await processBatchInStorage(nextPageDoc);
}
await sleep(1000 + Math.random() * 1500);
if (i % 5 === 0) {
localStorage.setItem('animeCardsData', JSON.stringify(storedData));
}
}
}
}
localStorage.setItem('animeCardsData', JSON.stringify(storedData));
completeProgressNotification(notificationId, 'Кеширование завершено. Всего в кеше: ' + countCurrentlyCachedCards(), true);
await processCards();
} catch (error) {
console.error("Ошибка в scrapeAllPages:", error);
completeProgressNotification(notificationId, 'Ошибка кеширования страниц.', false);
}
}
async function getCardId(cardElement) {
let cardId = cardElement.getAttribute('data-card-id') || cardElement.getAttribute('card-id');
if (!cardId && cardElement.tagName === 'A' && typeof cardElement.hasAttribute === 'function' && cardElement.hasAttribute('href')) {
const href = cardElement.getAttribute('href');
if (href) {
let match = href.match(/\/cards\/users\/\?id=(\d+)/);
if (match && match[1]) {
cardId = match[1];
} else {
match = href.match(/\/cards\/(\d+)\/users\//);
if (match && match[1]) {
cardId = match[1];
}
}
}
}
if (!cardId && typeof cardElement.matches === 'function') {
if (cardElement.matches('.anime-cards__item') || cardElement.matches('.lootbox__card')) {
cardId = cardElement.getAttribute('data-id');
} else if (cardElement.matches('.remelt__inventory-item')) {
const instanceIdFromRemelt = cardElement.getAttribute('data-id');
if (instanceIdFromRemelt) {
const canonicalIdFromCache = getCanonicalIdFromCacheByItemInstanceId(instanceIdFromRemelt);
if (canonicalIdFromCache) {
cardId = canonicalIdFromCache;
} else {
console.warn(`Не найден канонический ID в кеше для remelt item с instanceId ${instanceIdFromRemelt}.`);
}
}
}
}
if (!cardId && cardElement.tagName !== 'A') {
const linkElement = cardElement.querySelector('a[href*="/cards/users/?id="], a[href*="/cards/"][href*="/users/"]');
if (linkElement) {
const href = linkElement.getAttribute('href');
let match = href.match(/\/cards\/users\/\?id=(\d+)/);
if (match && match[1]) {
cardId = match[1];
} else {
match = href.match(/\/cards\/(\d+)\/users\//);
if (match && match[1]) {
cardId = match[1];
}
}
}
}
return cardId;
}
async function getFirstCardByOwner(ownerId) {
const storedData = JSON.parse(localStorage.getItem('animeCardsData')) || {};
const key = 'o_' + ownerId;
return storedData[key]?.[0] || null;
}
async function readyToCharge() {
const buttonId = 'readyToCharge';
displayNotification(buttonId, 'Подготовка к отметке карт...', 'progress', {sticky: true});
let cardsOnPage = getCardsOnPage();
if (!cardsOnPage || cardsOnPage.length === 0) { completeProgressNotification(buttonId, 'Карты на странице не найдены.', false); return; }
const cardsToProcess = cardsOnPage.filter(cardEl => !cardEl.classList.contains('trade__inventory-item--lock'));
const totalCardsToProcess = cardsToProcess.length;
if (totalCardsToProcess === 0) { completeProgressNotification(buttonId, 'Нет карт для отметки.', false); return; }
updateNotificationProgress(buttonId, 'Отмечаем карт:', 0, totalCardsToProcess);
startAnimation(buttonId); clearMarkFromCards(); cardCounter = 0;
let successfullyProcessedCount = 0, attemptedToProcessCount = 0;
for (const cardElement of cardsToProcess) {
cardElement.classList.add('charging-card');
let idToSend = cardElement.getAttribute('data-owner-id') || await getCardId(cardElement);
attemptedToProcessCount++;
if (idToSend) {
await sleep(1000 + Math.random() * 500);
try { if (await readyToChargeCard(idToSend)) successfullyProcessedCount++; }
catch (error) { console.error("Ошибка при отметке карты " + idToSend + ":", error); }
}
updateNotificationProgress(buttonId, 'Обработано карт:', attemptedToProcessCount, totalCardsToProcess);
cardElement.classList.remove('charging-card');
}
completeProgressNotification(buttonId, `Отправлено на обмен ${cardCounter} из ${successfullyProcessedCount} (${attemptedToProcessCount} попыток).`, true, 5000);
stopAnimation(buttonId);
}
async function readyToChargeCard(card_id_to_send) {
try {
await sleep(DELAY * 2 + Math.random() * DELAY);
const data = await $.ajax({ url: "/engine/ajax/controller.php?mod=trade_ajax", type: "post", data: { action: "propose_add", type: 1, card_id: card_id_to_send, user_hash: dle_login_hash }, dataType: "json", cache: false });
if (data?.error) {
if (data.error === 'Слишком часто, подождите пару секунд и повторите действие') {
await sleep(2500 + Math.random() * 1000); return await readyToChargeCard(card_id_to_send);
}
console.warn(`Ошибка от сервера (карта ${card_id_to_send}): ${data.error}`); return false;
}
if (data?.status == "added") { cardCounter++; return true; }
if (data?.status == "deleted") { await sleep(1000); return await readyToChargeCard(card_id_to_send); }
console.warn(`Неожиданный ответ от сервера для карты ${card_id_to_send}:`, data); return false;
} catch (e) {
console.error(`readyToChargeCard AJAX/исключение (ID ${card_id_to_send}):`, e.statusText || e.message || e);
return false;
}
}
function createPromoCodeButton() {
const domain = getCurrentDomain();
const promoUrl = domain + "/promo_codes";
const buttonLink = document.createElement('a');
buttonLink.id = 'promoCodeLinkButton';
buttonLink.href = promoUrl;
buttonLink.className = 'anim-interactive-button promo-code-button-custom';
const icon = document.createElement('span');
icon.className = 'fal fa-gift';
const text = document.createElement('span');
text.textContent = 'Промокоды';
buttonLink.appendChild(icon);
buttonLink.appendChild(text);
return buttonLink;
}
const style = document.createElement('style');
style.textContent = `
@keyframes glowEffect {
0% { box-shadow: 0 0 5px #6c5ce7; }
50% { box-shadow: 0 0 20px #6c5ce7; }
100% { box-shadow: 0 0 5px #6c5ce7; }
}
@keyframes glowChargeEffect {
0% { box-shadow: 0 0 7px #4CAF50; }
50% { box-shadow: 0 0 25px #4CAF50; }
100% { box-shadow: 0 0 7px #4CAF50; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes breatheShadowInteractive {
0% { box-shadow: 0 0 6px rgba(108, 92, 231, 0.2); transform: scale(1); }
50% { box-shadow: 0 0 12px rgba(108, 92, 231, 0.5); transform: scale(1.02); }
100% { box-shadow: 0 0 6px rgba(108, 92, 231, 0.2); transform: scale(1); }
}
@keyframes pulseWorkingBorderInteractive {
0% { box-shadow: 0 0 0 0px rgba(86, 200, 239, 0.7), 0 3px 8px rgba(0,0,0,0.25); }
70% { box-shadow: 0 0 0 10px rgba(86, 200, 239, 0), 0 5px 12px rgba(0,0,0,0.3); }
100% { box-shadow: 0 0 0 0px rgba(86, 200, 239, 0), 0 3px 8px rgba(0,0,0,0.25); }
}
@keyframes pulseIcon {
0% { transform: scale(1) rotate(0deg); opacity: 1; }
50% { transform: scale(1.2) rotate(0deg); opacity: 0.7; }
100% { transform: scale(1) rotate(0deg); opacity: 1; }
}
@keyframes cardHelperSpin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.processing-card {
position: relative;
}
.processing-card img {
position: relative;
z-index: 2;
}
.processing-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 8px;
z-index: 1;
animation: glowEffect 1.5s infinite;
pointer-events: none;
}
.charging-card {
position: relative;
}
.charging-card img {
position: relative;
z-index: 2;
}
.charging-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 8px;
z-index: 1;
animation: glowChargeEffect 1.5s infinite;
pointer-events: none;
}
.card-stats {
position: relative;
background: linear-gradient(34deg, #4e2264 0%, #943aca 55%);
padding: 8px;
color: white;
font-size: 12px;
margin-top: 5px;
border-radius: 5px;
display: flex;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
animation: fadeInUp 0.3s ease;
z-index: 0 !important;
box-shadow: 0px 0px 8px 0px #a367dc;
border: 1px dashed #ffffff !important;
}
.card-stats--minimalistic {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-stats--full {
flex-direction: column;
align-items: flex-start;
padding: 10px;
}
.card-stats--full .stat-line {
display: flex;
align-items: center;
margin-bottom: 4px;
}
.card-stats--full .stat-line:last-child {
margin-bottom: 0;
}
.card-stats--full .stat-line i.fas {
margin-right: 6px;
font-size: 13px;
width: 16px;
text-align: center;
}
.history__inner {
max-width: 1200px !important;
margin: 0 auto !important;
padding: 15px !important;
}
.history__item {
background: rgba(108, 92, 231, 0.05) !important;
border-radius: 10px !important;
padding: 20px !important;
margin-bottom: 20px !important;
}
.history__body {
display: flex !important;
flex-wrap: wrap !important;
gap: 15px !important;
padding: 15px !important;
border-radius: 8px !important;
}
.history__body--gained {
background: rgba(46, 213, 115, 0.1) !important;
margin-bottom: 10px !important;
}
.history__body--lost {
background: rgba(255, 71, 87, 0.1) !important;
}
@media screen and (min-width: 769px) {
.history__body-item {
width: 120px !important;
height: auto !important;
transition: transform 0.2s !important;
}
.history__body-item img {
width: 120px !important;
height: auto !important;
border-radius: 8px !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
}
}
.history__body-item:hover {
transform: scale(1.05) !important;
z-index: 2 !important;
}
.card-stats span {
display: flex;
align-items: center;
gap: 4px;
}
.card-stats span i {
font-size: 14px;
}
.lootbox__card {
position: relative !important;
transform: scale(0.85) !important;
margin-top: -15px !important;
margin-bottom: 35px !important;
}
.lootbox__card .card-stats {
position: absolute !important;
bottom: -35px !important;
left: 0 !important;
right: 0 !important;
margin: 0;
padding: 8px !important;
border-radius: 5px;
z-index: 9999 !important;
background: linear-gradient(45deg, #6c5ce7, #a367dc) !important;
font-size: 16px !important;
width: 100% !important;
transform: none !important;
text-rendering: optimizeLegibility !important;
-webkit-font-smoothing: antialiased !important;
}
.lootbox__card .card-stats span {
color: white !important;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3) !important;
padding: 0 8px !important;
flex: 1;
text-align: center;
font-weight: 500 !important;
}
.lootbox__card .card-stats i {
color: white !important;
font-size: 16px !important;
margin-right: 4px;
}
.lootbox__list {
gap: 25px !important;
padding-bottom: 20px !important;
}
@media screen and (max-width: 768px) {
.history__body-item,
.history__body-item img {
width: 100px !important;
}
.processing-card::before,
.charging-card::before {
top: -1px !important;
left: -1px !important;
right: -1px !important;
bottom: -1px !important;
opacity: 0.5 !important;
}
div[style*="position: fixed"][style*="right: 1%"] {
transform: scale(0.9);
transform-origin: bottom right;
}
.anim-interactive-button {
width: 40px !important;
height: 40px !important;
}
.anim-interactive-button span[class*="fa-"] {
font-size: 18px !important;
}
#promoCodeLinkButton.anim-interactive-button.promo-code-button-custom {
padding: 0 !important;
}
#promoCodeLinkButton.anim-interactive-button.promo-code-button-custom span:not(.fal) {
display: none !important;
}
#promoCodeLinkButton.anim-interactive-button.promo-code-button-custom .fal {
margin-right: 0 !important;
}
#promoCodeLinkButton {
right: 1%;
}
#promoCodeLinkButton,
#toggleStatsModeButton {
transform: scale(0.9);
transform-origin: bottom left;
}
.anim-button-tooltip {
font-size: 11px !important;
padding: 5px 8px !important;
}
.card-stats {
padding: 4px;
font-size: 10px;
}
.card-stats span i {
font-size: 12px !important;
}
.remelt__inventory-list {
grid-template-columns: repeat(2, 1fr) !important;
gap: 10px !important;
}
.remelt__inventory-item {
width: 100% !important;
margin: 0 !important;
}
.remelt__inventory-item img {
width: 100% !important;
height: auto !important;
}
.remelt__inventory-item .card-stats {
width: 100% !important;
margin-top: 4px !important;
}
.lootbox__card {
transform: scale(0.8) !important;
margin-top: -20px !important;
margin-bottom: 30px !important;
}
}
.anim-interactive-button {
background-color: #6c5ce7;
color: #fff;
border: none;
border-radius: 50%;
width: 45px;
height: 45px;
padding: 0;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: all 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex;
justify-content: center;
align-items: center;
animation: breatheShadowInteractive 2.5s infinite ease-in-out;
outline: none;
position: relative;
text-decoration: none;
}
.anim-interactive-button span[class*="fa-"] {
display: inline-block;
font-size: 20px;
transition: transform 0.25s ease-out;
}
.anim-interactive-button:hover {
background-color: #5f51e3;
transform: scale(1.12) translateY(-3px);
box-shadow: 0 7px 18px rgba(0, 0, 0, 0.25);
}
.anim-interactive-button:hover span[class*="fa-"] {
transform: rotate(18deg);
}
.anim-interactive-button:active {
background-color: #5245c9;
transform: scale(0.93) translateY(0px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition-duration: 0.08s;
}
.anim-interactive-button:active span[class*="fa-"] {
transform: rotate(-8deg) scale(0.88);
}
.anim-interactive-button.is-working {
animation: pulseWorkingBorderInteractive 1s infinite ease-in-out, breatheShadowInteractive 2.5s infinite ease-in-out paused !important;
}
.anim-interactive-button.is-working:hover {
transform: scale(1.05) translateY(-1px);
}
.anim-button-tooltip {
position: absolute;
right: calc(100% + 10px);
top: 50%;
transform: translateY(-50%) translateX(10px);
background-color: #2d3436;
color: #fff;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
opacity: 0;
transition: opacity 0.25s ease, transform 0.25s ease;
white-space: nowrap;
z-index: 1001;
pointer-events: none;
}
.card-helper-status-notification {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #3e444c;
color: #f0f0f0;
padding: 10px 18px;
border-radius: 6px;
font-size: 14px;
font-family: Arial, sans-serif;
z-index: 2147483647;
display: flex;
align-items: center;
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
opacity: 0;
transition: opacity 0.4s ease, bottom 0.4s ease;
max-width: 380px;
min-width: 280px;
box-sizing: border-box;
}
.card-helper-status-notification.show {
opacity: 1;
bottom: 30px;
}
.ch-status-icon-container {
margin-right: 10px;
display: flex;
align-items: center;
height: 18px;
}
.card-helper-spinner {
width: 16px;
height: 16px;
border: 2px solid #666;
border-top: 2px solid #ddd;
border-radius: 50%;
animation: cardHelperSpin 0.8s linear infinite;
}
.card-helper-checkmark,
.card-helper-crossmark {
font-size: 18px;
line-height: 1;
}
.card-helper-checkmark {
color: #76c779;
}
.card-helper-crossmark {
color: #e57373;
}
.card-helper-status-text {
white-space: normal;
text-align: left;
line-height: 1.3;
}
.promo-code-button-custom {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
width: auto;
height: auto;
padding: 10px 18px;
border-radius: 25px;
text-decoration: none;
font-size: 14px;
line-height: 1.2;
}
.promo-code-button-custom .fal {
margin-right: 8px;
font-size: 16px;
}
.toggle-stats-button {
position: fixed;
bottom: 70px;
left: 20px;
z-index: 9998;
width: auto;
height: auto;
padding: 8px 15px;
border-radius: 20px;
text-decoration: none;
font-size: 13px;
line-height: 1.2;
}
.toggle-stats-button .fal {
margin-right: 6px;
font-size: 14px;
}
`;
document.head.appendChild(style);
function clearIcons() {
$('.card-notification:first')?.click();
}
function autoRepeatCheck() {
clearIcons();
checkGiftCard(document);
const volumeButton = document.querySelector('.adv_volume.volume_on');
if (volumeButton) {
volumeButton.click();
}
}
async function checkGiftCard(doc) {
const button = doc.querySelector('#gift-icon');
if (!button) return;
const giftCode = button.getAttribute('data-code');
if (!giftCode) return;
try {
const response = await fetch('/engine/ajax/controller.php?mod=gift_code_game', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ code: giftCode, user_hash: dle_login_hash })
});
const data = await response.json();
if (data.status === 'ok') {
showTemporaryMessage('giftStatus', data.text, true);
button.remove();
} else if (data.text) {
showTemporaryMessage('giftStatus', data.text, false);
}
} catch (error) {
console.error("Error checking gift card:", error);
showTemporaryMessage('giftError', "Ошибка проверки гифт карты.", false);
}
}
async function checkNewCard() {
const userHash = window.dle_login_hash;
if (!userHash) {
setTimeout(() => {
if (window.dle_login_hash) {
checkNewCard();
}
}, 2000);
return;
}
const currentDateTime = new Date();
const localStorageKey = 'checkCardStopped' + userHash;
const currentHourMarker = currentDateTime.toISOString().slice(0, 13);
if (localStorage.getItem(localStorageKey) === currentHourMarker) {
return;
}
try {
await sleep(DELAY * 2);
const cardForWatchPayload = {
user_hash: userHash
};
const responseText = await $.ajax({
url: "/ajax/card_for_watch/",
type: "post",
data: cardForWatchPayload,
cache: false
});
if (typeof responseText === 'string') {
let jsonData;
if (responseText.startsWith("cards{") && responseText.endsWith("}")) {
try {
const jsonString = responseText.substring(5);
jsonData = JSON.parse(jsonString);
} catch (e) {
}
} else {
}
if (jsonData && jsonData.if_reward && jsonData.if_reward.toLowerCase() === "yes") {
if (jsonData.reward_limit !== undefined && parseInt(jsonData.reward_limit, 10) === 0) {
localStorage.setItem(localStorageKey, currentHourMarker);
}
}
}
} catch (e) {
let errorMsg = "Ошибка автосбора: ";
if (e.status !== undefined) errorMsg += `HTTP ${e.status} `;
if (e.statusText) errorMsg += `${e.statusText} `;
}
}
async function setCache(key, data, baseTtlInSeconds = 86400) {
const jitterPercent = 0.10;
const jitter = Math.round(baseTtlInSeconds * jitterPercent * (Math.random() * 2 - 1));
const finalTtlInSeconds = baseTtlInSeconds + jitter;
const expires = Date.now() + finalTtlInSeconds * 1000;
const cacheData = { data, expires };
try {
localStorage.setItem(key, JSON.stringify(cacheData));
} catch (e) {
console.error("Ошибка при записи в localStorage (возможно, переполнен):", e);
showTemporaryMessage('localStorageError', 'Ошибка записи в localStorage.', false);
}
}
async function getCache(key) {
const cacheDataJSON = localStorage.getItem(key);
if (!cacheDataJSON) return null;
try {
const cacheData = JSON.parse(cacheDataJSON);
if (!cacheData || typeof cacheData !== 'object' || !cacheData.expires || !('data' in cacheData) || Date.now() > cacheData.expires) {
localStorage.removeItem(key); return null;
}
return cacheData.data;
} catch (e) {
localStorage.removeItem(key); return null;
}
}
async function cacheCard(key, data) { await setCache(key, data); }
async function getCard(key) { return await getCache(key); }
function clearCardCache() {
let clearedCount = 0, animeCardsDataCleared = false;
Object.keys(localStorage).forEach(key => {
if (key.startsWith('cardId: ')) {
try { const parsed = JSON.parse(localStorage.getItem(key)); if (parsed?.data && parsed.expires) { localStorage.removeItem(key); clearedCount++; } } catch (e) {}
} else if (key === 'animeCardsData') { localStorage.removeItem(key); animeCardsDataCleared = true; }
});
showTemporaryMessage('cacheCleared', `Очищено ${clearedCount} карт. ${animeCardsDataCleared ? "Общий кеш очищен." : ""}`, true);
}
(function() {
'use strict';
function initializeScript() {
if (typeof $ === 'undefined') { console.error("jQuery не найден."); }
if (typeof dle_login_hash === 'undefined') console.warn("dle_login_hash не определена.");
addUpdateButton();
setInterval(autoRepeatCheck, 2000);
setInterval(checkNewCard, 15000);
$('#tg-banner')?.remove(); try { localStorage.setItem('notify18', 'closed'); localStorage.setItem('hideTelegramAs', 'true'); } catch (e) {}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initializeScript);
else initializeScript();
})();