您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display KRW approximation next to USD on bitget
// ==UserScript== // @license Unlicense // @name Show KRW on Bitget // @namespace https://greasyfork.org/ // @version 202412270042 // @description Display KRW approximation next to USD on bitget // @author funyaba // @match https://www.bitget.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=bitget.com // @connect m.stock.naver.com // @connect api.upbit.com // @grant GM.xmlHttpRequest // @noframes // ==/UserScript== /* jshint esversion: 8 */ /* global WeakRef */ (async function() { 'use strict'; // Your code here... async function resolveAllObject(obj) { const promises = Object.values(obj).map(fn => fn()); const results = await Promise.all(promises); return Object .fromEntries(Object.keys(obj).map((key, i) => [key, results[i]])); } class ExchangeService { static async getInstanceAsync() { const cache = new Map(); const apis = { ['USDKRW']: { fetcher: async () => { const { response } = await GM.xmlHttpRequest({ method: 'GET', url: 'https://m.stock.naver.com' + '/front-api/marketIndex/productDetail' + '?category=exchange&reutersCode=FX_USDKRW', responseType: 'json', }); const { result } = response; return parseFloat(result.calcPrice); }, interval: 1000 * 60 * 60, }, ['USDTKRW']: { fetcher: async () => { const { response: [{ trade_price: tradePrice }] } = await GM.xmlHttpRequest({ method: 'GET', url: 'https://api.upbit.com/v1/ticker?markets=KRW-USDT', responseType: 'json', }); return tradePrice; }, interval: 1000, }, }; const fetchers = Object.fromEntries(Object.entries(apis).map(([k, v]) => [k, v.fetcher])); const resolved = await resolveAllObject(fetchers); for (const [key, value] of Object.entries(resolved)) { cache.set(key, value); } for (const [key, value] of Object.entries(apis)) { setInterval(async () => { cache.set(key, await value.fetcher()); }, value.interval); } return { get(key) { return cache.get(key); }, }; } } const exchangeService = await ExchangeService.getInstanceAsync(); const numberFormatter = new Intl.NumberFormat('en', { maximumFractionDigits: 0, }); const numberRegex = /(-?(?:[0-9]{1,3},)*[0-9]{1,3}(?:\.?[0-9]*))/g; const replaceValue = (targetRef) => { let target = targetRef.deref(); try { if (!target) return; let text = target.nodeValue.trim(); if (text.includes('KRW') || text.includes('₩')) return; let matches = null; if (text === 'USD' || text === 'USDT') { if (!target.parentElement || !target.parentElement.parentElement) return; const parentText = target.parentElement.parentElement.innerText; if (!parentText) return; matches = parentText.match(numberRegex); } else { matches = text.match(numberRegex); } if (!matches) return; let price = NaN; switch (true) { case text.endsWith('USD'): price = exchangeService.get('USDKRW'); break; case text.endsWith('USDT'): case text.includes('₮'): price = exchangeService.get('USDTKRW'); break; default: return; } if (isNaN(price)) return; const value = matches[0].replace(/,/g, ''); const converted = numberFormatter.format(parseFloat(value) * price); const formatted = text.includes('₮') ? `${text} (₩${converted})` : `${text} (${converted} KRW)`; target.nodeValue = formatted; } finally { target = null; } }; const observerConfig = { childList: true, characterData: true, subtree: true }; let mutationTargetRefs = []; let isScheduled = false; const callback = (mutations, observer) => { mutations.forEach((mutation) => { if (mutation.target.nodeName.toUpperCase() === 'SVG') return; mutationTargetRefs.push(new WeakRef(mutation.target)); }); if (isScheduled) return; isScheduled = true; requestAnimationFrame(() => { observer.disconnect(); const nodes = new WeakSet(); for (let i = 0; i < mutationTargetRefs.length; ++i) { let mutationTarget = mutationTargetRefs[i].deref(); try { if (!mutationTarget) { mutationTargetRefs[i] = null; continue; } if (nodes.has(mutationTarget)) continue; nodes.add(mutationTarget); switch (mutationTarget.nodeType) { case Node.TEXT_NODE: replaceValue(new WeakRef(mutationTarget)); nodes.add(mutationTarget); break; case Node.ELEMENT_NODE: { if (!mutationTarget.innerHTML.match(numberRegex)) break; const treeWalker = document.createTreeWalker( mutationTarget, NodeFilter.SHOW_TEXT, ); let node; while (treeWalker.nextNode()) { node = treeWalker.currentNode; try { if (nodes.has(node)) continue; if (!node.isConnected) continue; if ( !node.parentElement || node.parentElement.closest('textarea, [contenteditable="true"]') ) continue; replaceValue(new WeakRef(node)); } finally { nodes.add(node); node = null; } } break; } default: break; } } finally { mutationTarget = null; } } mutationTargetRefs = []; isScheduled = false; observer.observe(document.body, observerConfig); }); }; const observer = new MutationObserver(callback); observer.observe(document.body, observerConfig); })();