MT_Player_Stats_Widget

Скрипт добавляет виджет для отображения статистики игрока на форуме игры "Мир танков"

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name        MT_Player_Stats_Widget
// @namespace 	http://tampermonkey.net/
// @version 	2.5
// @description Скрипт добавляет виджет для отображения статистики игрока на форуме игры "Мир танков"
// @author 		Qwen2.5-Max, Nikolay (Next) Bespalov, and many thanks to @luxero for the information provided ;)
// @match 		http://forum.tanki.su/index.php?/topic/*
// @grant 		GM_xmlhttpRequest
// @grant 		GM_setValue
// @grant 		GM_getValue
// @grant 		GM_listValues
// @grant 		GM_deleteValue
// @connect 	tanki.su
// @connect 	lesta.ru
// @license 	MIT
// @icon64 		data:image/png;base64,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
// ==/UserScript==

(function() {
    'use strict';

    // Предварительная загрузка шрифта
    const fontLink = document.createElement('link');
    fontLink.rel = 'stylesheet';
    fontLink.href = 'https://next-ru.github.io/player_widget/MTSans-RegularCondensed.ttf';
    document.head.appendChild(fontLink);

    // Предварительная загрузка фонового изображения
    const bgImage = new Image();
    bgImage.src = 'https://next-ru.github.io/player_widget/bg-content.png';

    // Время жизни кэша (5 минут)
    const CACHE_TTL = 5 * 60 * 1000;

    // Функция для очистки устаревшего кэша
    function clearOldCache() {
        const now = Date.now();
        const keys = GM_listValues().filter(key => key.startsWith('playerCache_'));
        keys.forEach(key => {
            const cacheData = GM_getValue(key);
            if(now - cacheData.timestamp > CACHE_TTL) {
                GM_deleteValue(key);
            }
        });
    }

    // Вызов функции очистки кэша при запуске скрипта
    clearOldCache();

    // Добавление пользовательского шрифта и стилей
    const style = document.createElement('style');
    style.innerHTML = ` @font-face {
            font-family: 'MTSans RegularCondensed';
            src: url(${fontLink.href}) format('truetype');
            font-weight: 400;
            font-style: normal;
    }
    .regular-condensed {
            font-family: 'MTSans RegularCondensed', sans-serif;
            font-weight: 400;
            font-style: normal;
    }
    .widget-container {
            display: flex;
            flex-direction: column;
            gap: 16px;
    }
    .widget-top,
    .widget-bottom {
            display: flex;
            gap: 8px;
            align-items: flex-start;
    }
    .widget-block.player {
            width: 250px;
    }
    .widget-block.clan {
            min-width: 250px;
            max-width: 300px;
            flex-grow: 1;
    }
    .value {
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
    }
    .clan-emblem {
            display: flex;
            justify-content: flex-end;
    }
    .clan-emblem img {
            width: 64px;
            height: 64px;
    }
    `;
    document.head.appendChild(style);

    // Создание элемента для виджета
    let widget = document.createElement('div');
    widget.id = 'player-widget'; // Уникальный ID для виджета
    widget.style.position = 'absolute'; // Позиционирование будет обновляться динамически
    widget.style.backgroundImage = `url(${bgImage.src})`; // Устанавливаем фоновое изображение
    widget.style.backgroundColor = '#000'; // Задаем запасной цвет фона (черный)
    widget.style.color = '#fff'; // Задаем цвет текста (белый)
    widget.style.padding = '8px'; // Добавляем внутренние отступы для лучшей читаемости
    widget.style.borderRadius = '8px'; // Добавляем скругление углов
    widget.style.fontSize = '16px'; // Устанавливаем размер шрифта для текста
    widget.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)'; // Добавляем тень для объемного эффекта
    widget.style.whiteSpace = 'nowrap'; // Запрещаем перенос текста на новую строку
    widget.style.display = 'none'; // Скрываем виджет при создании
    widget.classList.add('regular-condensed'); // Добавляем CSS-класс для дополнительных стилей
    document.body.appendChild(widget);

    // Функция для обновления позиции виджета
    function updateWidgetPosition(widget, target) {
        const offsetX = 0; // Отступ по горизонтали (в пикселях)
        const offsetY = 15; // Отступ по вертикали (в пикселях)

        const targetRect = target.getBoundingClientRect(); // Получаем координаты целевого элемента
        const widgetRect = widget.getBoundingClientRect(); // Получаем размеры виджета

        let leftPosition = targetRect.right + window.scrollX + offsetX; // Виджет справа от элемента с отступом
        let topPosition;

        // Определяем, где находится целевой элемент: в верхней или нижней части экрана
        const isTargetInUpperHalf = targetRect.top < (window.innerHeight / 2);

        if (isTargetInUpperHalf) {
            // Если целевой элемент в верхней половине экрана, виджет рисуем снизу
            topPosition = targetRect.bottom + window.scrollY + offsetY;
        } else {
            // Если целевой элемент в нижней половине экрана, виджет рисуем сверху
            topPosition = targetRect.top - widgetRect.height + window.scrollY - offsetY;
        }

        // Проверяем, выходит ли виджет за пределы экрана
        if (topPosition < 0) {
            // Если виджет выходит за верхний край, перемещаем его вниз
            topPosition = targetRect.bottom + window.scrollY + offsetY;
        } else if (topPosition + widgetRect.height > window.innerHeight + window.scrollY) {
            // Если виджет выходит за нижний край, перемещаем его вверх
            topPosition = targetRect.top - widgetRect.height + window.scrollY - offsetY;
        }

        // Применяем стили
        Object.assign(widget.style, {
            top: `${topPosition}px`,
            left: `${leftPosition}px`
    });
    }

    // Функция для форматирования времени из Unix-формата
    function formatUnixTime(timestamp) {
        const date = new Date(timestamp * 1000);
        return date.toLocaleString();
    }

    // Функция для обновления содержимого виджета
    function updateWidgetContent(nickname, spaId, regTimestamp, lastBattleAt, summaryData, statisticsData, clanId, clanInfo, playerClanInfo) {
        // Верхняя секция: информация об игроке
        const playerTopInfo = `
        <div class="value">Никнейм: ${nickname}&nbsp;<a href="https://tanki.su/ru/community/accounts/${spaId}-${nickname}/" target="_blank"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAG5JREFUGBmFjlEKwCAMQ2fZYfwbG+z+JxD8UPE2XStm64ay/iR9SaFumUytlW3kBEQBG6D33sGr3gdqcs67DeFRaooFIdTyadGWcPj80MmwpNk0+BxS33+lFUspx6iZUjrBVzGRiIK8APZSZg4KLkdURXcsOD5MAAAAAElFTkSuQmCC" alt="Профиль игрока" style="vertical-align: middle;"></a></div>
        <div class="value">Дата регистрации: ${regTimestamp ? formatUnixTime(regTimestamp) : 'Неизвестно'}</div>
        <div class="value">Последний бой: ${lastBattleAt ? formatUnixTime(lastBattleAt) : 'Неизвестно'}</div>
        `;

        // Верхняя секция: эмблема клана (если есть)
        const clanTopInfo = clanInfo ? `
        <img src="https://lesta.ru${clanInfo.large_emblem_url}?nocache=${Date.now()}" alt="Эмблема клана">
        ` : '';

        // Нижняя секция: статистика игрока
        const playerBottomInfo = `
        <div class="value">Личный рейтинг: ${summaryData?.global_rating ?? 'Неизвестно'}</div>
        <div class="value">Бои: ${statisticsData?.battles_count ?? 'Неизвестно'}</div>
        <div class="value">Победы: ${statisticsData?.wins_count_percent != null ? `${statisticsData.wins_count_percent}%` : 'Неизвестно'}</div>
        <div class="value">Средний урон: ${statisticsData?.damage_dealt_avg ?? 'Неизвестно'}</div>
        <div class="value">Попадания: ${summaryData?.hits_ratio != null ? `${summaryData.hits_ratio}%` : 'Неизвестно'}</div>
        <div class="value">Средний опыт за бой: ${statisticsData?.xp_amount_avg ?? 'Неизвестно'}</div>
        <div class="value">Максимум уничтожено за бой: ${statisticsData?.frags_max ?? 'Неизвестно'}</div>
        <div class="value">Максимальный опыт за бой: ${statisticsData?.xp_max ?? 'Неизвестно'}</div>
        <div class="value">Максимальный урон за бой: ${statisticsData?.damage_max ?? 'Неизвестно'}</div>
        <div class="value">Знаки классности «Мастер»: ${summaryData?.mastery ? `${summaryData.mastery.mastery_count}/${summaryData.mastery.vehicles_count}` : 'Неизвестно'}</div>
        `;

        // Нижняя секция: статистика клана (если есть)
        const clanBottomInfo = clanInfo ? `
        <div class="value">Клан-тег: [${clanInfo.tag ?? 'Неизвестно'}]</div>
        <div class="value">Название: ${clanInfo.name ?? 'Неизвестно'}</div>
        <div class="value">Должность: ${playerClanInfo?.localized_name ?? 'Неизвестно'}</div>
        <div class="value">Дней в клане: ${playerClanInfo?.days_in_clan ?? 'Неизвестно'}</div>
        <div class="value">Рейтинг клана: ${clanInfo.rating ?? '-'}</div>
        <div class="value">Позиция клана в общем рейтинге: ${clanInfo.rating_position ?? '-'}</div>
        <div class="value">Активные игроки: ${clanInfo.members_count ?? 'Неизвестно'}</div>
        <div class="value">Среднее количество боёв: ${clanInfo.average_battles_count ?? 'Неизвестно'}</div>
        <div class="value">Средний опыт за бой: ${clanInfo.average_xp_per_battle ?? 'Неизвестно'}</div>
        <div class="value">Средний урон за бой: ${clanInfo.average_damage_per_battle ?? 'Неизвестно'}</div>
        <div class="value">Средний процент побед: ${clanInfo?.average_win_rate != null ? `${clanInfo.average_win_rate}%` : 'Неизвестно'}</div>
        ` : '';

        // Вставляем всё в виджет
        widget.innerHTML = `
        <div class="widget-container">
                <!-- Верхняя часть -->
                <div class="widget-top">
                        <div class="widget-block player">${playerTopInfo}</div>
                        ${clanTopInfo ? `<div class="widget-block clan clan-emblem">${clanTopInfo}</div>` : ''}
                </div>
                <!-- Нижняя часть -->
                <div class="widget-bottom">
                        <div class="widget-block player">${playerBottomInfo}</div>
                        ${clanBottomInfo ? `<div class="widget-block clan">${clanBottomInfo}</div>` : ''}
                </div>
        </div>
        `;
    }

    // Функция для получения данных из кэша
    function getCache(key) {
        const cacheData = GM_getValue(`playerCache_${key}`);
        if(cacheData && Date.now() - cacheData.timestamp < CACHE_TTL) {
            return cacheData.data;
        }
        return null;
    }

    // Функция для сохранения данных в кэш
    function setCache(key, value) {
        GM_setValue(`playerCache_${key}`, {
            timestamp: Date.now(),
            data: value
        });
    }

    // Функция для отображения небольшого текстового сообщения в виджете
    function showWidgetMessage(text) {
        widget.innerHTML = text; // Устанавливаем текст сообщения
        widget.style.display = 'block'; // Делаем виджет видимым
        widget.style.minWidth = '200px'; // Минимальная ширина
        widget.style.minHeight = '50px'; // Минимальная высота
        widget.style.display = 'flex'; // Используем flexbox для центрирования
        widget.style.justifyContent = 'center'; // Центрируем по горизонтали
        widget.style.alignItems = 'center'; // Центрируем по вертикали
    }

    // Функция для получения данных об игроке
    async function fetchData(spaId, nickname) {
        const cacheKey = `${spaId}_${nickname}`;
        const cachedData = getCache(cacheKey);
        if(cachedData) {
            updateWidgetContent(
                nickname,
                spaId,
                cachedData.regTimestamp,
                cachedData.lastBattleAt,
                cachedData.summaryData,
                cachedData.statisticsData,
                cachedData.clanId,
                cachedData.clanInfo,
                cachedData.playerClanInfo
            );
            return;
        }

        let regTimestamp = null;
        let lastBattleAt = null;
        let clanId = null;
        let summaryData = null;
        let statisticsData = null;
        let clanInfo = null;
        let playerClanInfo = null;

        // Параллельные запросы: profileResponse, summaryData и statisticsData
        const [profileResponse, summaryPromise, statisticsPromise] = await Promise.all([
            // Запрос данных профиля игрока
            (async () => {
                try {
                    const response = await new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: `https://tanki.su/ru/community/accounts/${spaId}-${nickname}/`,
                            headers: {
                                "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
                                "accept-language": "ru-RU,ru;q=0.9",
                                "priority": "u=0, i",
                                "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
                                "sec-ch-ua-mobile": "?0",
                                "sec-ch-ua-platform": "\"Windows\"",
                                "sec-fetch-dest": "document",
                                "sec-fetch-mode": "navigate",
                                "sec-fetch-site": "same-origin",
                                "sec-fetch-user": "?1",
                                "upgrade-insecure-requests": "1"
                            },
                            referrer: "https://tanki.su/ru/community/accounts/",
                            referrerPolicy: "unsafe-url",
                            onload: resolve,
                            onerror: reject
                        });
                    });

                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, "text/html");

                    const scripts = doc.querySelectorAll('script');
                    for(const script of scripts) {
                        const scriptContent = script.textContent;
                        if(scriptContent.includes("USER_DATA")) {
                            const regTimestampMatch = scriptContent.match(/"reg_timestamp":\s*(\d+)/);
                            const lastBattleAtMatch = scriptContent.match(/"last_battle_at":\s*(\d+)/);
                            const clanIdMatch = scriptContent.match(/"id":\s*(\d+)/);

                            if(regTimestampMatch) regTimestamp = parseInt(regTimestampMatch[1], 10);
                            if(lastBattleAtMatch) lastBattleAt = parseInt(lastBattleAtMatch[1], 10);
                            if(clanIdMatch) clanId = parseInt(clanIdMatch[1], 10);
                            break;
                        }
                    }
                } catch {}
            })(),

            // Запрос данных о суммарной статистике игрока
            (async () => {
                try {
                    const response = await new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: `https://tanki.su/wotup/profile/summary/?spa_id=${spaId}&battle_type=random`,
                            headers: {
                                "accept": "*/*",
                                "accept-language": "ru-RU,ru;q=0.9",
                                "priority": "u=1, i",
                                "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
                                "sec-ch-ua-mobile": "?0",
                                "sec-ch-ua-platform": "\"Windows\"",
                                "sec-fetch-dest": "empty",
                                "sec-fetch-mode": "cors",
                                "sec-fetch-site": "same-origin",
                                "x-requested-with": "XMLHttpRequest"
                            },
                            referrer: `https://tanki.su/ru/community/accounts/${spaId}-${nickname}/`,
                            referrerPolicy: "unsafe-url",
                            onload: resolve,
                            onerror: reject
                        });
                    });

                    const rawSummaryData = JSON.parse(response.responseText)?.data ?? null;
                    if(rawSummaryData) {
                        summaryData = {
                            global_rating: rawSummaryData.global_rating,
                            hits_ratio: rawSummaryData.hits_ratio,
                            mastery: rawSummaryData.mastery
                        };
                    }
                } catch {}
            })(),

            // Запрос данных о статистике игрока
            (async () => {
                try {
                    const response = await new Promise((resolve, reject) => {
                        GM_xmlhttpRequest({
                            method: "GET",
                            url: `https://tanki.su/wotup/profile/statistics/?spa_id=${spaId}&battle_type=random`,
                            headers: {
                                "accept": "*/*",
                                "accept-language": "ru-RU,ru;q=0.9",
                                "priority": "u=1, i",
                                "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
                                "sec-ch-ua-mobile": "?0",
                                "sec-ch-ua-platform": "\"Windows\"",
                                "sec-fetch-dest": "empty",
                                "sec-fetch-mode": "cors",
                                "sec-fetch-site": "same-origin",
                                "x-requested-with": "XMLHttpRequest"
                            },
                            referrer: `https://tanki.su/ru/community/accounts/${spaId}-${nickname}/`,
                            referrerPolicy: "unsafe-url",
                            onload: resolve,
                            onerror: reject
                        });
                    });

                    const rawStatisticsData = JSON.parse(response.responseText)?.data ?? null;
                    if(rawStatisticsData) {
                        statisticsData = {
                            damage_max: rawStatisticsData.damage_max,
                            wins_count_percent: rawStatisticsData.wins_count_percent,
                            battles_count: rawStatisticsData.battles_count,
                            damage_dealt_avg: rawStatisticsData.damage_dealt_avg,
                            xp_amount_avg: rawStatisticsData.xp_amount_avg,
                            frags_max: rawStatisticsData.frags_max,
                            xp_max: rawStatisticsData.xp_max
                        };
                    }
                } catch {}
            })()
        ]);

        // Если игрок в клане, запрашиваем информацию о клане
        if(clanId) {
            const [clanPromise, playersPromise] = await Promise.all([
                // Запрос информации о клане
                (async () => {
                    try {
                        const response = await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: "GET",
                                url: `https://lesta.ru/clans/wot/${clanId}/api/claninfo/`,
                                headers: {
                                    "accept": "application/json, text/javascript, */*; q=0.01",
                                    "accept-language": "ru-RU,ru;q=0.9",
                                    "priority": "u=1, i",
                                    "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
                                    "sec-ch-ua-mobile": "?0",
                                    "sec-ch-ua-platform": "\"Windows\"",
                                    "sec-fetch-dest": "empty",
                                    "sec-fetch-mode": "cors",
                                    "sec-fetch-site": "same-origin",
                                    "x-requested-with": "XMLHttpRequest"
                                },
                                referrer: `https://lesta.ru/clans/wot/${clanId}/`,
                                referrerPolicy: "strict-origin-when-cross-origin",
                                onload: resolve,
                                onerror: reject
                            });
                        });

                        const clanData = JSON.parse(response.responseText)?.clanview ?? null;
                        if(clanData) {
                            clanInfo = {
                                tag: clanData.clan.tag,
                                name: clanData.clan.name,
                                rating: clanData.rating.rating,
                                average_battles_count: clanData.rating.average_battles_count,
                                rating_position: clanData.rating.rating_position,
                                average_xp_per_battle: clanData.rating.average_xp_per_battle,
                                average_damage_per_battle: clanData.rating.average_damage_per_battle,
                                average_win_rate: clanData.rating.average_win_rate,
                                members_count: clanData.clan.members_count,
                                large_emblem_url: clanData.clan.large_emblem_url
                            };
                        }
                    } catch {}
                })(),

                // Запрос информации о игроке в клане
                (async () => {
                    try {
                        const response = await new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: "GET",
                                url: `https://lesta.ru/clans/wot/${clanId}/api/players/?offset=0&limit=25&o=-role&timeframe=all&battle_type=default`,
                                headers: {
                                    "accept": "application/json, text/javascript, */*; q=0.01",
                                    "accept-language": "ru-RU,ru;q=0.9",
                                    "priority": "u=1, i",
                                    "sec-ch-ua": "\"Not(A:Brand\";v=\"99\", \"Google Chrome\";v=\"133\", \"Chromium\";v=\"133\"",
                                    "sec-ch-ua-mobile": "?0",
                                    "sec-ch-ua-platform": "\"Windows\"",
                                    "sec-fetch-dest": "empty",
                                    "sec-fetch-mode": "cors",
                                    "sec-fetch-site": "same-origin",
                                    "x-requested-with": "XMLHttpRequest"
                                },
                                referrer: `https://lesta.ru/clans/wot/${clanId}/players/`,
                                referrerPolicy: "strict-origin-when-cross-origin",
                                onload: resolve,
                                onerror: reject
                            });
                        });

                        const playersData = JSON.parse(response.responseText)?.items ?? null;
                        const player = playersData.find(p => p.id === parseInt(spaId, 10));
                        if(player) {
                            playerClanInfo = {
                                localized_name: player.role.localized_name,
                                days_in_clan: player.days_in_clan
                            };
                        }
                    } catch {}
                })()
            ]);
        }

        // Сохранение данных в кэш
        const playerData = {
            regTimestamp,
            lastBattleAt,
            summaryData,
            statisticsData,
            clanId,
            clanInfo,
            playerClanInfo
        };

        setCache(cacheKey, playerData);

        // Обновление содержимого виджета
        updateWidgetContent(
            nickname,
            spaId,
            playerData.regTimestamp,
            playerData.lastBattleAt,
            playerData.summaryData,
            playerData.statisticsData,
            playerData.clanId,
            playerData.clanInfo,
            playerData.playerClanInfo
        );
    }

    let widgetTimeout = null; // Переменная для хранения таймера
    const WIDGET_TIMEOUT_DELAY = 200; // Задержка перед исчезновением виджета (в миллисекундах)

    // Обработчик наведения мыши на элемент или виджет
    function handleHover(event) {
        const target = event.target;

        // Проверяем, находится ли курсор на элементе
        if (
            target.tagName === 'SPAN' &&
            target.parentElement &&
            target.parentElement.classList.contains('ccw-word')
        ) {
            const parentSpan = target.parentElement;
            const nickname = parentSpan.getAttribute('data-value');
            const authorInfo = parentSpan.closest('.author_info');
            if (authorInfo) {
                const parentAnchor = authorInfo.querySelector('a[hovercard-spaid]');
                if (parentAnchor) {
                    const spaId = parentAnchor.getAttribute('hovercard-spaid');

                    // Отменяем предыдущий таймер, если он был запущен
                    if (widgetTimeout) {
                        clearTimeout(widgetTimeout);
                        widgetTimeout = null;
                    }

                    // Показываем индикатор загрузки
                    showWidgetMessage('Загрузка...');
                    updateWidgetPosition(widget, target);

                    // Загружаем данные и обновляем позицию виджета
                    fetchData(spaId, nickname).then(() => {
                        updateWidgetPosition(widget, target);
                    });
                } else {
                    // Добавляем обработку случая, когда элемент hovercard-spaid не найден
                    showWidgetMessage('SPA ID не найден. Аккаунт удалён.');
                    updateWidgetPosition(widget, target);
                }
            }
        }

        // Если курсор находится на самом виджете, отменяем таймер
        if (target === widget || widget.contains(target)) {
            if (widgetTimeout) {
                clearTimeout(widgetTimeout);
                widgetTimeout = null;
            }
        }
    }

    // Обработчик ухода курсора с элемента или виджета
    function handleMouseOut(event) {
        const target = event.target;

        // Если курсор ушел с элемента или с виджета
        if (
            (target.tagName === 'SPAN' &&
             target.parentElement.classList.contains('ccw-word')) ||
            target === widget ||
            widget.contains(target)
        ) {
            // Запускаем таймер для скрытия виджета
            widgetTimeout = setTimeout(() => {
                widget.innerHTML = ''; // Очищаем содержимое виджета
                widget.style.display = 'none'; // Скрываем виджет
                widgetTimeout = null; // Сбрасываем таймер
            }, WIDGET_TIMEOUT_DELAY);
        }
    }

    document.addEventListener('mouseover', handleHover);
    document.addEventListener('mouseout', handleMouseOut);
})();