MSU 包包小精靈

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

  1. // ==UserScript==
  2. // @name MSU 包包小精靈
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.64
  5. // @author Alex from MyGOTW
  6. // @description 擷取 MSU.io 物品價格與庫存
  7. // @match https://msu.io/*
  8. // @grant none
  9. // @run-at document-end
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. function waitForElement(selector) {
  17. return new Promise(resolve => {
  18. // 如果元素已存在,直接返回
  19. if (document.querySelector(selector)) {
  20. return resolve(document.querySelector(selector));
  21. }
  22.  
  23. // 建立 observer 監聽 DOM 變化
  24. const observer = new MutationObserver(mutations => {
  25. if (document.querySelector(selector)) {
  26. observer.disconnect();
  27. resolve(document.querySelector(selector));
  28. }
  29. });
  30.  
  31. observer.observe(document.body, {
  32. childList: true,
  33. subtree: true
  34. });
  35. });
  36. }
  37.  
  38. async function initialize() {
  39. console.log('Initialize being called with URL:', window.location.href);
  40. if (!window.location.href.includes('/marketplace/inventory/')) {
  41. return;
  42. }
  43.  
  44. // 添加 API 監聽
  45. const originalFetch = window.fetch;
  46. window.fetch = async function(...args) {
  47. const [resource, config] = args;
  48. // 檢查是否為目標 API
  49. if (typeof resource === 'string' &&
  50. resource.includes('/marketplace/api/marketplace/inventory/') &&
  51. resource.includes('/owned')) {
  52. try {
  53. const response = await originalFetch.apply(this, args);
  54. const clone = response.clone();
  55. const jsonData = await clone.json();
  56. // 將數據保存在全局變數中
  57. window.inventoryData = jsonData;
  58. console.log('已保存背包資料:', jsonData);
  59. return response;
  60. } catch (error) {
  61. console.error('監聽 API 時發生錯誤:', error);
  62. return originalFetch.apply(this, args);
  63. }
  64. }
  65. return originalFetch.apply(this, args);
  66. };
  67.  
  68. try {
  69. // 等待目標元素出現
  70. const targetNode = await waitForElement('div[class*="item-list"]');
  71. const observer = new MutationObserver((mutations) => {
  72. mutations.forEach((mutation) => {
  73. if (mutation.addedNodes.length) {
  74. getNFTitem();
  75. }
  76. });
  77. });
  78.  
  79. observer.observe(targetNode, {
  80. childList: true,
  81. subtree: true
  82. });
  83. // 初始執行一次
  84. getNFTitem();
  85. } catch (error) {
  86. console.error('Error initializing:', error);
  87. }
  88. }
  89.  
  90. const getNFTitem = () => {
  91. addStyleToHead();
  92. const articles = document.querySelectorAll('div[class*="item-list"] > article');
  93. let currentActiveBtn = null;
  94. let isClickable = true;
  95. let allButtons = []; // 新增儲存所有按鈕的陣列
  96.  
  97. articles.forEach((article, index) => {
  98. if (article.querySelector('.click-btn')) return;
  99.  
  100. const nameSpanElement = article.querySelector('.leave-box div div span:first-child');
  101. if (nameSpanElement && nameSpanElement.innerText) {
  102. const fragment = document.createDocumentFragment();
  103.  
  104. let textDiv = document.createElement('div');
  105. textDiv.textContent = '查看市場價格';
  106. textDiv.className = 'click-btn';
  107. allButtons.push(textDiv); // 將按鈕加入陣列
  108.  
  109. textDiv.onclick = async () => {
  110. if (!isClickable) return;
  111.  
  112. isClickable = false;
  113. // 設定所有按鈕為禁用狀態
  114. allButtons.forEach(btn => {
  115. btn.style.cursor = 'not-allowed';
  116. });
  117.  
  118. const originalText = textDiv.textContent;
  119. textDiv.textContent = '';
  120. textDiv.classList.add('loading');
  121. const itemName = filterNFTitem(nameSpanElement.innerText);
  122. const itemCategoryNo = window.inventoryData.records[index].category.categoryNo
  123. const searchItem = {
  124. name:itemName,
  125. categoryNo:itemCategoryNo ? itemCategoryNo : null
  126. }
  127. const result = await fetchItme(searchItem);
  128. textDiv.classList.remove('loading');
  129. textDiv.textContent = result;
  130.  
  131. setTimeout(() => {
  132. isClickable = true;
  133. // 恢復所有按鈕的狀態
  134. allButtons.forEach(btn => {
  135. btn.style.cursor = 'pointer';
  136. });
  137. textDiv.textContent = originalText;
  138. }, 3000);
  139. }
  140.  
  141. fragment.appendChild(textDiv);
  142. article.insertBefore(fragment.firstChild, article.firstChild);
  143.  
  144. article.addEventListener('mouseenter', () => {
  145. if (currentActiveBtn && currentActiveBtn !== textDiv) {
  146. currentActiveBtn.style.display = 'none';
  147. }
  148. textDiv.style.display = 'block';
  149. currentActiveBtn = textDiv;
  150. });
  151. }
  152. });
  153. }
  154.  
  155. const filterNFTitem = (name) =>{
  156. const match = name.match(/^(.*?)(?=#|$)/);
  157. if (match) {
  158. return match[1].trim(); // 保留中間的空格,但去除前後空格
  159. }
  160. return name;
  161. }
  162. const addStyleToHead = () => {
  163. const style = document.createElement('style');
  164. const css = `
  165. .click-btn {
  166. width: 100%;
  167. background-color: rebeccapurple;
  168. border-radius: 5px;
  169. cursor: pointer;
  170. color: white;
  171. padding: 5px;
  172. text-align: center;
  173. margin-top: 5px;
  174. transition: background-color 0.3s, cursor 0.3s;
  175. display: none;
  176. z-index: 9999;
  177. }
  178. .click-btn:hover {
  179. background-color: #663399;
  180. }
  181.  
  182. /* 新增載入動畫相關樣式 */
  183. .loading {
  184. position: relative;
  185. min-height: 24px;
  186. }
  187. .loading::after {
  188. content: '';
  189. position: absolute;
  190. width: 20px;
  191. height: 20px;
  192. top: 50%;
  193. left: 50%;
  194. margin-top: -10px;
  195. margin-left: -10px;
  196. border: 2px solid #ffffff;
  197. border-radius: 50%;
  198. border-top-color: transparent;
  199. animation: spin 0.8s linear infinite;
  200. }
  201. @keyframes spin {
  202. to {
  203. transform: rotate(360deg);
  204. }
  205. }
  206. `;
  207. style.textContent = css;
  208. document.head.appendChild(style);
  209. }
  210.  
  211. const getLowestPriceItem = (priceData, exactName) => {
  212. console.log(exactName)
  213. if (!priceData?.items || priceData.items.length === 0) {
  214. return null;
  215. }
  216.  
  217. // 只篩選完全符合名稱的物品
  218. const exactMatches = priceData.items.filter(item => item.name === exactName);
  219.  
  220. if (exactMatches.length === 0) {
  221. return null;
  222. }
  223.  
  224. return exactMatches.reduce((lowest, current) => {
  225. const currentPrice = BigInt(current.salesInfo?.priceWei || '0');
  226. const lowestPrice = BigInt(lowest.salesInfo?.priceWei || '0');
  227.  
  228. return currentPrice < lowestPrice ? current : lowest;
  229. }, exactMatches[0]);
  230. }
  231.  
  232. const fetchItme = async(item) => {
  233. try {
  234. const searchResult = await fetch("https://msu.io/marketplace/api/marketplace/explore/items", {
  235. headers: {
  236. "accept": "*/*",
  237. "cache-control": "no-cache",
  238. "content-type": "application/json",
  239. "sec-fetch-dest": "empty",
  240. "sec-fetch-mode": "cors",
  241. "sec-fetch-site": "same-origin"
  242. },
  243. body: JSON.stringify({
  244. filter: {
  245. name:item.name,
  246. categoryNo:item.categoryNo,
  247. level:{min:0, max: 250},
  248. potential:{min:0, max: 4},
  249. price:{min:0, max: 10000000000},
  250. starforce:{min:0, max: 25}
  251. },
  252. sorting: "ExploreSorting_LOWEST_PRICE",
  253. paginationParam: { pageNo: 1, pageSize: 135 }
  254. }),
  255. method: "POST",
  256. mode: "cors",
  257. credentials: "include"
  258. });
  259.  
  260. const priceData = await searchResult.json();
  261. const lowestPriceItem = getLowestPriceItem(priceData, item.name);
  262. const fullPrice = lowestPriceItem ?
  263. (BigInt(lowestPriceItem.salesInfo.priceWei) / BigInt(1e18))
  264. .toString() + '.' +
  265. (BigInt(lowestPriceItem.salesInfo.priceWei) % BigInt(1e18))
  266. .toString()
  267. .padStart(18, '0')
  268. .slice(0, 6)
  269. .replace(/\.?0+$/, '')
  270. :
  271. null;
  272. return fullPrice ? `${fullPrice} Neso` : '無上架資料'
  273. } catch (error) {
  274. console.error(`查詢 ${itemName} 價格時發生錯誤:`, error);
  275. return '查詢錯誤,被鎖啦'
  276. }
  277. }
  278. initialize()
  279. // URL 變化監聽
  280. const originalPushState = history.pushState;
  281. const originalReplaceState = history.replaceState;
  282. history.pushState = function (...args) {
  283. originalPushState.apply(this, args);
  284. handleUrlChange('pushState');
  285. };
  286. history.replaceState = function (...args) {
  287. originalReplaceState.apply(this, args);
  288. handleUrlChange('replaceState');
  289. };
  290. window.addEventListener('popstate', function () {
  291. handleUrlChange('popstate');
  292. });
  293. function handleUrlChange(method) {
  294. console.log(`小精靈通知: [${method}] URL 已變化: ${window.location.href}`);
  295. initialize();
  296. }
  297.  
  298. })();
  299.