MSU 包包小精靈

擷取 MSU.io 物品價格與庫存

// ==UserScript==
// @name         MSU 包包小精靈
// @namespace    http://tampermonkey.net/
// @version      0.64
// @author       Alex from MyGOTW
// @description  擷取 MSU.io 物品價格與庫存
// @match        https://msu.io/*
// @grant        none
// @run-at       document-end
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    function waitForElement(selector) {
        return new Promise(resolve => {
            // 如果元素已存在,直接返回
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            // 建立 observer 監聽 DOM 變化
            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    async function initialize() {
        console.log('Initialize being called with URL:', window.location.href);
        if (!window.location.href.includes('/marketplace/inventory/')) {
            return;
        }

        // 添加 API 監聽
        const originalFetch = window.fetch;
        window.fetch = async function(...args) {
            const [resource, config] = args;
            
            // 檢查是否為目標 API
            if (typeof resource === 'string' && 
                resource.includes('/marketplace/api/marketplace/inventory/') && 
                resource.includes('/owned')) {
                
                try {
                    const response = await originalFetch.apply(this, args);
                    const clone = response.clone();
                    const jsonData = await clone.json();
                    
                    // 將數據保存在全局變數中
                    window.inventoryData = jsonData;
                    console.log('已保存背包資料:', jsonData);
                    
                    return response;
                } catch (error) {
                    console.error('監聽 API 時發生錯誤:', error);
                    return originalFetch.apply(this, args);
                }
            }
            
            return originalFetch.apply(this, args);
        };

        try {
            // 等待目標元素出現
            const targetNode = await waitForElement('div[class*="item-list"]');
            
            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    if (mutation.addedNodes.length) {
                        getNFTitem();
                    }
                });
            });

            observer.observe(targetNode, {
                childList: true,
                subtree: true
            });
            
            // 初始執行一次
            getNFTitem();
        } catch (error) {
            console.error('Error initializing:', error);
        }
    }

    const getNFTitem = () => {
        addStyleToHead();
        const articles = document.querySelectorAll('div[class*="item-list"] > article');
        let currentActiveBtn = null;
        let isClickable = true;
        let allButtons = []; // 新增儲存所有按鈕的陣列

        articles.forEach((article, index) => {
            if (article.querySelector('.click-btn')) return;

            const nameSpanElement = article.querySelector('.leave-box div div span:first-child');
            if (nameSpanElement && nameSpanElement.innerText) {
                const fragment = document.createDocumentFragment();

                let textDiv = document.createElement('div');
                textDiv.textContent = '查看市場價格';
                textDiv.className = 'click-btn';
                allButtons.push(textDiv); // 將按鈕加入陣列

                textDiv.onclick = async () => {
                    if (!isClickable) return;

                    isClickable = false;
                    // 設定所有按鈕為禁用狀態
                    allButtons.forEach(btn => {
                        btn.style.cursor = 'not-allowed';
                    });

                    const originalText = textDiv.textContent;
                    textDiv.textContent = '';
                    textDiv.classList.add('loading');
                    const itemName = filterNFTitem(nameSpanElement.innerText);
                    const itemCategoryNo =  window.inventoryData.records[index].category.categoryNo
                    const searchItem = {
                        name:itemName,
                        categoryNo:itemCategoryNo ? itemCategoryNo : null
                    }
                    const result = await fetchItme(searchItem);
                    textDiv.classList.remove('loading');
                    textDiv.textContent = result;

                    setTimeout(() => {
                        isClickable = true;
                        // 恢復所有按鈕的狀態
                        allButtons.forEach(btn => {
                            btn.style.cursor = 'pointer';
                        });
                        textDiv.textContent = originalText;
                    }, 3000);
                }

                fragment.appendChild(textDiv);
                article.insertBefore(fragment.firstChild, article.firstChild);

                article.addEventListener('mouseenter', () => {
                    if (currentActiveBtn && currentActiveBtn !== textDiv) {
                        currentActiveBtn.style.display = 'none';
                    }
                    textDiv.style.display = 'block';
                    currentActiveBtn = textDiv;
                });
            }
        });
    }

    const filterNFTitem = (name) =>{
        const match = name.match(/^(.*?)(?=#|$)/);
        if (match) {
            return match[1].trim(); // 保留中間的空格,但去除前後空格
        }
        return name;
    }
    const addStyleToHead = () => {
        const style = document.createElement('style');
        const css = `
            .click-btn {
                width: 100%;
                background-color: rebeccapurple;
                border-radius: 5px;
                cursor: pointer;
                color: white;
                padding: 5px;
                text-align: center;
                margin-top: 5px;
                transition: background-color 0.3s, cursor 0.3s;
                display: none;
                z-index: 9999;
            }
            .click-btn:hover {
                background-color: #663399;
            }

            /* 新增載入動畫相關樣式 */
            .loading {
                position: relative;
                min-height: 24px;
            }
            .loading::after {
                content: '';
                position: absolute;
                width: 20px;
                height: 20px;
                top: 50%;
                left: 50%;
                margin-top: -10px;
                margin-left: -10px;
                border: 2px solid #ffffff;
                border-radius: 50%;
                border-top-color: transparent;
                animation: spin 0.8s linear infinite;
            }
            @keyframes spin {
                to {
                    transform: rotate(360deg);
                }
            }
        `;
        style.textContent = css;
        document.head.appendChild(style);
    }

    const getLowestPriceItem = (priceData, exactName) => {
        console.log(exactName)
        if (!priceData?.items || priceData.items.length === 0) {
            return null;
        }

        // 只篩選完全符合名稱的物品
        const exactMatches = priceData.items.filter(item => item.name === exactName);

        if (exactMatches.length === 0) {
            return null;
        }

        return exactMatches.reduce((lowest, current) => {
            const currentPrice = BigInt(current.salesInfo?.priceWei || '0');
            const lowestPrice = BigInt(lowest.salesInfo?.priceWei || '0');

            return currentPrice < lowestPrice ? current : lowest;
        }, exactMatches[0]);
    }

    const fetchItme = async(item) => {
        try {
            const searchResult = await fetch("https://msu.io/marketplace/api/marketplace/explore/items", {
                headers: {
                    "accept": "*/*",
                    "cache-control": "no-cache",
                    "content-type": "application/json",
                    "sec-fetch-dest": "empty",
                    "sec-fetch-mode": "cors",
                    "sec-fetch-site": "same-origin"
                },
                body: JSON.stringify({
                    filter: { 
                        name:item.name,
                        categoryNo:item.categoryNo,
                        level:{min:0, max: 250},
                        potential:{min:0, max: 4},
                        price:{min:0, max: 10000000000},
                        starforce:{min:0, max: 25}
                     },
                    sorting: "ExploreSorting_LOWEST_PRICE",
                    paginationParam: { pageNo: 1, pageSize: 135 }
                }),
                method: "POST",
                mode: "cors",
                credentials: "include"
            });

            const priceData = await searchResult.json();
            const lowestPriceItem = getLowestPriceItem(priceData, item.name);
            const fullPrice = lowestPriceItem ?
                (BigInt(lowestPriceItem.salesInfo.priceWei) / BigInt(1e18))
                .toString() + '.' +
                (BigInt(lowestPriceItem.salesInfo.priceWei) % BigInt(1e18))
                .toString()
                .padStart(18, '0')
                .slice(0, 6)
                .replace(/\.?0+$/, '')
                :
                null;
            return fullPrice ? `${fullPrice} Neso` : '無上架資料'
        } catch (error) {
            console.error(`查詢 ${itemName} 價格時發生錯誤:`, error);
            return '查詢錯誤,被鎖啦'
        }
    }
    initialize()
    // URL 變化監聽
    const originalPushState = history.pushState;  
    const originalReplaceState = history.replaceState;  
    
    history.pushState = function (...args) {  
        originalPushState.apply(this, args);  
        handleUrlChange('pushState');
    };  
    
    history.replaceState = function (...args) {  
        originalReplaceState.apply(this, args);  
        handleUrlChange('replaceState');
    };  
    
    window.addEventListener('popstate', function () {  
        handleUrlChange('popstate');  
    });  
    
    function handleUrlChange(method) {  
        console.log(`小精靈通知: [${method}] URL 已變化: ${window.location.href}`);  
        initialize();
    }

})();