pummmm / pummmmPro Gmgn

// ==UserScript==
// @name         pummmmPro Gmgn
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  PUMMMMMPro高级工具提示增强
// @author       You
// @match        https://gmgn.ai/?*
// @match        https://gmgn.ai/sol/token/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @connect      pummmmpro.com
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    GM_addStyle(`
        .custom-tooltip {
            display: none;
            position: absolute;
            left: 100%;
            top: 100%;
            margin-left: -50px;
            margin-top: -20px;
            background-color: #1e1e2d;
            border: 1px solid #2d2d3d;
            border-radius: 12px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.3);
            z-index: 1000;
            width: 1400px;
            max-height: 600px;
            overflow: auto;
            font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
            color: #e0e0e0;
            padding: 20px;
            transition: opacity 0.2s ease;
        }
        .custom-tooltip table {
            width: 100%;
            border-collapse: separate;
            border-spacing: 0;
            font-size: 13px;
        }
        .custom-tooltip tr {
            line-height: 0.3;
        }
        .custom-tooltip th {
            background-color: #2a2a3a;
            color: #a0a0c0;
            padding: 12px 15px;
            text-align: left;
            font-weight: 500;
            top: 0;
            z-index: 10;
        }
        .custom-tooltip td {
            padding: 10px 15px;
            border-bottom: 1px solid #2d2d3d;
        }
        .custom-tooltip tr:hover td {
            background-color: #2a2a3a;
        }
        .custom-tooltip .address-link {
            color: #4a8cff;
            text-decoration: none;
            transition: color 0.2s;
            font-family: 'Consolas', monospace;
        }
        .custom-tooltip .address-link:hover {
            color: #6ba2ff;
            text-decoration: underline;
        }
        .custom-tooltip .percentage-cell {
            color: #4caf50;
            font-weight: 500;
        }
        .custom-tooltip .hide-red {
            color: #ff6b6b;
        }
        .custom-tooltip .hide-green {
            color: #4caf50;
        }
        .custom-tooltip .tooltip-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #2d2d3d;
        }
        .custom-tooltip .tooltip-title {
            font-size: 16px;
            font-weight: 600;
            color: #ffffff;
        }
        .custom-tooltip .tooltip-time {
            font-size: 12px;
            color: #a0a0c0;
        }
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        .tooltip-loading {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100px;
        }
        .spinner {
            width: 40px;
            height: 40px;
            border: 4px solid rgba(255,255,255,0.1);
            border-radius: 50%;
            border-top-color: #4a6bff;
            animation: spin 1s linear infinite;
        }
        .token-input {
            width: 100%;
            padding: 10px;
            margin: 10px 0;
            background-color: #2a2a3a;
            border: 1px solid #3d3d4d;
            border-radius: 6px;
            color: #ffffff;
            font-family: 'Segoe UI', sans-serif;
        }

        .token-dialog {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: #1e1e2d;
            border: 1px solid #2d2d3d;
            border-radius: 12px;
            padding: 20px;
            z-index: 9999;
            width: 400px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.3);
        }
        .token-dialog h3 {
            margin-top: 0;
            color: #ffffff;
        }
        .token-input {
            width: 150px;
            height:30px;
            padding: 10px;
            margin: 10px 0;
            background-color: #2a2a3a;
            border: 1px solid #3d3d4d;
            border-radius: 6px;
            color: #ffffff;
            font-family: 'Segoe UI', sans-serif;
        }
        .token-submit {
            background-color: #4a6bff;
            color: white;
            border: none;
            padding: 10px 15px;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 500;
            height:44px;
            transition: background-color 0.2s;
        }
        .token-submit:hover {
            background-color: #5a7bff;
        }

    `);


    let USER_TOKEN = GM_getValue('pummmmpro_token', null);

    function showTokenDialog() {
        const dialog = document.createElement('div');
        dialog.className = 'token-dialog';
        dialog.innerHTML = `
            <h3>PUMMMMPro</h3>
            <input type="text" class="token-input" placeholder="Enter your Token">
            <button class="token-submit">Save</button>
        `;

        document.body.appendChild(dialog);
        const input = dialog.querySelector('.token-input');
        const submitBtn = dialog.querySelector('.token-submit');

        submitBtn.addEventListener('click', () => {
            const token = input.value.trim();
            if (!token) {
                dialog.remove();
                return;
            }
            if (token.length < 8) {
                dialog.remove();
                return;
            }
            USER_TOKEN = token;
            dialog.remove();
        });
    }


    // 初始路由检测
    checkAndExecute();
    // 监听后续路由变化(SPA 跳转)
    window.addEventListener('popstate', checkAndExecute);
    const observer = new MutationObserver(checkAndExecute);
    observer.observe(document.body, { childList: true, subtree: true });


    function checkAndExecute() {
        if (window.location.href.match(/https:\/\/gmgn\.ai\/sol\/token\/.+/)) {
            executeTokenPageScript();
        } else if (window.location.href.match(/https:\/\/gmgn\.ai\/\?.*/)) {
            executeScript();
        }
    }

    function executeScript() {
        const parentSelector = '.transition-colors.flex-shrink-0';
        const parents = document.querySelectorAll(parentSelector);

        parents.forEach(parent => {
            const textElements = parent.querySelectorAll('.text-\\[14px\\]');

            textElements.forEach(el => {
                if (el.nextElementSibling?.classList.contains('new-span')) {
                    return;
                }

                const newSpan = document.createElement('span');
                newSpan.className = 'new-span';
                Object.assign(newSpan.style, {
                    position: 'absolute',
                    color: '#e96061',
                    marginTop: '0px',
                    marginLeft: '200px'
                });
                newSpan.textContent = 'Query';
                el.parentNode.insertBefore(newSpan, el.nextSibling);
                addClickTooltip(newSpan, el.textContent);
            });
        });
    }


     function executeTokenPageScript() {
        let el1 = document.querySelector('.flex.items-center.cursor-pointer.gap-x-4px.text-text-300.text-sm.font-normal.group');
        if (!el1) {
            el1 = document.querySelector('.flex.items-center.cursor-pointer.gap-x-4px.text-text-300.text-sm.font-normal.group');
            if(!el1){
                return;
            }
        }

        const el = document.querySelector('.new-buttonTn');
        if (el) {
            return;
        }

        const dragButton = document.createElement('button');
        dragButton.className = 'new-buttonTn';
        dragButton.textContent = 'Query';

         const savedPosition = JSON.parse(GM_getValue('dragButtonPosition', null)) || {
             left: '50px',
             top: '50px'
         };

        Object.assign(dragButton.style, {
            position: 'fixed',
            left: savedPosition.left,
            top: savedPosition.top,
            padding: '10px 20px',
            backgroundColor: '#4CAF50',
            color: 'white',
            border: 'none',
            borderRadius: '5px',
            cursor: 'move',
            zIndex: '9999',
            userSelect: 'none'
        });

        el1.appendChild(dragButton);

        let isDragging = false;
        let offsetX, offsetY;

        dragButton.addEventListener('mousedown', (e) => {
            isDragging = true;
            offsetX = e.clientX - dragButton.getBoundingClientRect().left;
            offsetY = e.clientY - dragButton.getBoundingClientRect().top;
            e.preventDefault();
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;

            let newX = e.clientX - offsetX;
            let newY = e.clientY - offsetY;

            // 限制按钮不超出视口
            newX = Math.max(0, Math.min(newX, window.innerWidth - dragButton.offsetWidth));
            newY = Math.max(0, Math.min(newY, window.innerHeight - dragButton.offsetHeight));

            dragButton.style.left = `${newX}px`;
            dragButton.style.top = `${newY}px`;
        });

        document.addEventListener('mouseup', () => {
             if (isDragging) {
                 isDragging = false;
                 dragButton.style.cursor = 'move';

                 // 保存位置到localStorage
                 GM_setValue('dragButtonPosition', JSON.stringify({
                     left: dragButton.style.left,
                     top: dragButton.style.top
                 }));
             }
        });

        // 在这里调用你想要触发的函数
        addClickTooltip(dragButton, el1.textContent.trim());

    }



    function addClickTooltip(trigger, spanText) {
        const tooltip = document.createElement('div');
        tooltip.className = 'custom-tooltip';
        document.body.appendChild(tooltip);

        trigger.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            if (!USER_TOKEN || typeof USER_TOKEN !== 'string' || USER_TOKEN.trim() === "") {
                showTokenDialog();
                return;
            }else{
                GM_setValue('pummmmpro_token', USER_TOKEN);
            }
            e.stopPropagation();
            tooltip.style.display = 'block';tooltip.style.position = 'fixed';tooltip.style.left = '50%';
            tooltip.style.top = '50%';tooltip.style.transform = 'translate(-50%, -50%)';tooltip.style.opacity = '0';
            setTimeout(() => { tooltip.style.opacity = '1'; }, 10);
            tooltip.innerHTML = `<div class="tooltip-loading"><div class="spinner"></div></div> `;

            GM_xmlhttpRequest({
                method: "GET",
                url: `https://pummmmpro.com/api/analysisVip/${USER_TOKEN}/${spanText}`,
                headers: {
                    "Accept": "application/json"
                },
                onload: function(response) {
                    if(response.status === 200){
                        const responseJson = JSON.parse(response.responseText);
                        const data = responseJson.tableData;
                        const ca = responseJson.ca;
                        tooltip.innerHTML = `
                        <div class="tooltip-header">
                            <div class="tooltip-title">PummmmmPro</div>
                            <div class="tooltip-title"><a href="https://x.com/search?q=${ca}&f=live" target="_blank" class="address-link" title="Hash"> 推特 </a></div>
                            <div class="tooltip-title"><a href="https://x3.pro/trending-tweets/coin-search?address=${ca}" target="_blank" class="address-link" title="Hash"> X3推特 </a></div>
                            <div class="tooltip-title"><button class="exit-button">退出账户</button></div>
                        </div>
                        <table>
                            <thead>
                                <tr>
                                    <th>地址</th>
                                    <th>余额</th>
                                    <th>钱包时长</th>
                                    <th>占比</th>
                                    <th>来源</th>
                                    <th>盈利</th>
                                    <th>买/卖</th>
                                    <th>买入时差</th>
                                    <th>操作时差</th>
                                    <th>持币顺序</th>
                                    <th>币均时</th>
                                    <th>时币数</th>
                                    <th>GasTips</th>
                                    <th>筹码类型</th>
                                    <th>Hash</th>
                                </tr>
                            </thead>
                            <tbody>
                                ${data.map(item => {
                            const fullAddress = item.owner || 'N/A';
                            const displayAddress = fullAddress.length > 8 ? `${fullAddress.substring(0, 6)}...${fullAddress.substring(fullAddress.length - 4)}` : fullAddress;
                            const fullSour = item.solSour || '';
                            const displaySour = fullSour.length > 20 ? `${fullSour.substring(0, 3)}...${fullSour.substring(fullSour.length - 2)}` : fullSour;
                            const profitChangeInt = item.profitChange > 0 ? `${item.profitChange }%` : '';
                            const walletAgeDays = item.initTime ? Math.floor((Date.now() - item.initTime * 1000) / (1000 * 60 * 60 * 24)): '0';
                            const tokenOrderClass = item.tokenOrderHide === 1 ? 'hide-red' : '';
                            const tipsClass = item.tipsHide === 1 ? 'hide-red' : '';
                            const activityTimeClass = item.activityTimeHide === 1 ? 'hide-red' : '';
                            const initDayClass = walletAgeDays < 2 ? 'hide-red' : '';
                            const outsideClass = item.outside ? 'hide-green' : '';
                            const activityTimeFormat = item.activityTime ? new Date(item.activityTime * 1000).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit',minute: '2-digit',second: '2-digit'}) : '-';
                            //const lastActivityTimeFormat = item.lastActivityTime ? new Date(item.lastActivityTime * 1000).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit',minute: '2-digit',second: '2-digit'}) : '-';
                            const currentTime = Date.now();
                            const activityTime = item.activityTime ? item.activityTime * 1000 : 0;
                            const timeDiffMinutes = activityTime ? Math.floor((currentTime - activityTime) / (1000 * 60)) : '-';
                            const lastActivityTime = item.lastActivityTime ? item.lastActivityTime * 1000 : 0;
                            const lastTimeDiffMinutes = lastActivityTime ? Math.floor((currentTime - lastActivityTime) / (1000 * 60)) : '-';
                            return `
                                        <tr>
                                            <td><a href="https://gmgn.ai/sol/address/${fullAddress}" target="_blank" class="address-link" title="${fullAddress}"> ${displayAddress}</a></td>
                                            <td>${item.amount || '0'}</td>
                                            <td class="${initDayClass}">${walletAgeDays}</td>
                                            <td class="percentage-cell">${item.ratio || '-'}%</td>
                                            <td>${fullSour.length > 10 ? `<a href="https://gmgn.ai/sol/address/${fullSour}" target="_blank" class="address-link" title="${fullSour}">${displaySour}</a>`: displaySour }</td>
                                            <td class="percentage-cell">${profitChangeInt}</td>
                                            <td>${item.buyTxCountCur}/${item.sellTxCountCur}</td>
                                            <td class="${activityTimeClass}">${activityTimeFormat} (${timeDiffMinutes})</td>
                                            <td>${lastTimeDiffMinutes}</td>
                                            <td class="${tokenOrderClass}">${item.tokenOrder || '-'}</td>
                                            <td>${item.holdTime || '-'}</td>
                                            <td>${item.tokenNum}</td>
                                            <td class="${tipsClass}">${item.tips || '-'}</td>
                                            <td><span>${item.within || ''}</span><span class="${outsideClass}">${item.outside || ''}</span></td>
                                            <td> <a href="https://solscan.io/tx/${item.hash}" target="_blank" class="address-link" title="Hash"> Hash </a></td>
                                        </tr>
                                    `;
                        }).join('')}
                            </tbody>
                        </table>
                    `;
                    }else{
                        tooltip.innerHTML = `
                        <div class="tooltip-header">
                            <div class="tooltip-title">PummmmmPro</div>
                            <div class="tooltip-title"><button class="exit-button">退出账户</button></div>
                        </div>
                        <div style=" color: #ff6b6b;padding: 20px;text-align: center;font-size: 14px;">
                            <div style="font-size: 24px; margin-bottom: 10px;">⚠️</div>
                            <div style="font-weight: 500; margin-bottom: 5px;">Data loading failed</div>
                            <div style="color: #a0a0c0; font-size: 12px;">使用次数已用完,请联系管理员。The usage has been exhausted. Please contact the administrator.</div>
                        </div>
                    `;
                    }
                },
                onerror: function(error) {
                    tooltip.innerHTML = `
                    <div class="tooltip-header">
                            <div class="tooltip-title">PummmmmPro</div>
                            <div class="tooltip-title"><button class="exit-button">退出账户</button></div>
                    </div>
                    <div style=" color: #ff6b6b;padding: 20px;text-align: center;font-size: 14px;">
                            <div style="font-size: 24px; margin-bottom: 10px;">⚠️</div>
                            <div style="font-weight: 500; margin-bottom: 5px;">Data loading failed</div>
                            <div style="color: #a0a0c0; font-size: 12px;">${error.message}</div>
                     </div>
                    `;
                }
            });
        });

        const hideTooltip = () => {
            tooltip.style.opacity = '0';
            setTimeout(() => { tooltip.style.display = 'none'; }, 300);
        };

        const exit = () => {
            tooltip.style.display = "none";
            USER_TOKEN = "";
            GM_setValue('pummmmpro_token', "");
        };

        document.addEventListener('click', (e) => {
            if (!tooltip.contains(e.target)) {
                hideTooltip();
                return;
            }
            if (e.target.classList.contains('exit-button')) {
                exit();
            }
        });

    }
})();