Card Helper AStars | AnimeStars | ASStars

Отображения спроса карт и Авто-Лут карточек с просмотра

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