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