Added currency conversion functionality.
// ==UserScript==
// @name Skeb enhance
// @namespace http://tampermonkey.net/
// @version 2026-05-14.2
// @description Added currency conversion functionality.
// @author xiyuesaves
// @license AGPLv3
// @match https://skeb.jp/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=skeb.jp
// @grant GM_xmlhttpRequest
// @connect cdn.jsdelivr.net
// ==/UserScript==
const localLang = navigator.language.split('-')[0] || "en"
const i18nMap = {
"zh": {
"currency": "cny",
"art": "插图",
"comic": "漫画",
"genre": "文本",
"novel": "小说",
"video": "视频",
"voice": "语音",
"correction": "建议",
"title": "委托开放前作者可随时修改价格",
"price": "建议金额",
"referenceOnly": "暂未开放,仅作参考",
"waiting": "获取中",
"error": "获取失败",
"loading": "加载中"
},
"en": {
"currency": "usd",
"art": "Art",
"comic": "Comic",
"genre": "Text",
"novel": "Novel",
"video": "Video",
"voice": "Voice",
"correction": "Correction",
"title": "Author can modify price before commission is open",
"price": "Suggested Amount",
"referenceOnly": "Not yet open, for reference only",
"waiting": "Waiting",
"error": "Failed to fetch",
"loading": "Loading"
},
"ja": {
"currency": "jpy",
"art": "イラスト",
"comic": "漫画",
"genre": "テキスト",
"novel": "小説",
"video": "動画",
"voice": "音声",
"correction": "修正",
"title": "委託が公開される前に作者は価格を変更可能です",
"price": "推奨金額",
"referenceOnly": "未公開、参考用のみ",
"waiting": "取得中",
"error": "取得失敗",
"loading": "読み込み中"
}
}
const targetCurrency = i18nMap[localLang]?.currency || i18nMap["en"].currency;
const container = createContainer();
const ajaxList = [];
const listenList = [];
const currencies = {
day: 0,
data: null
}
const originOpen = XMLHttpRequest.prototype.open;
const originSend = XMLHttpRequest.prototype.send;
const originHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function () {
this.addEventListener("load", function (obj) {
const url = obj.target.responseURL; // obj.target -> this
listenList.forEach((el) => {
if (el.rule.test(url)) {
const find = ajaxList.find((el) => el.xml === this);
if (find) {
find.url = url;
find.res = JSON.parse(this.response);
el.callback(find);
} else {
el.callback(false);
}
}
});
});
originOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function () {
const xml = ajaxList.find((el) => el.xml === this);
if (xml) {
xml.send = JSON.parse(arguments[0]);
}
originSend.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function () {
const xml = ajaxList.find((el) => el.xml === this);
if (xml) {
xml.header[arguments[0]] = arguments[1];
} else {
ajaxList.push({
xml: this,
url: "",
header: {
[arguments[0]]: arguments[1],
},
});
}
originHeader.apply(this, arguments);
};
function listenAjax(rule, callback) {
listenList.push({
rule,
callback,
});
}
function createContainer() {
const div = document.createElement("div")
div.className = "skeb-enhanced"
Object.assign(div.style, {
border: "solid 1px #cbd0d7",
borderRadius: "8px",
background: "#e4e6ea",
boxSizing: "border-box",
padding: "8px",
width: "100%",
display: "flex",
justifyContent: "space-between",
height: "auto",
marginBottom: "1.5rem"
})
const firstSelector = "#root > main > div > section:nth-child(1) > div > div > div.column.is-5 > div:nth-child(1) > div:nth-child(2)"
const scondSelector = "#root > main > div > section.section > div > div.columns > div.column.is-3 > div > div > table"
setInterval(() => {
if (document.querySelector(".skeb-enhanced")) {
return;
}
const divider = document.querySelector(firstSelector)
if (divider) {
divider.insertAdjacentElement("afterend", div)
return;
}
const table = document.querySelector(scondSelector)
if (table) {
table.insertAdjacentElement("beforebegin", div)
}
}, 100)
return div;
}
function formatted(price, lang = localLang) {
return new Intl.NumberFormat(lang, {
maximumFractionDigits: 20
}).format(price);
}
function gmRequest(url, options = {}) {
const { method = "GET", headers = {}, body, responseType = "text", timeout = 15000 } = options;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method,
url,
headers,
data: body,
responseType,
timeout,
withCredentials: true,
onload: (response) => {
response.ok = response.status >= 200 && response.status < 300;
response.text = () => Promise.resolve(response.responseText || "");
response.json = async () => JSON.parse(await response.text());
resolve(response);
},
onerror: reject,
ontimeout: () => reject(new Error(`request timeout: ${url}`)),
onabort: () => reject(new Error(`request abort: ${url}`)),
});
});
}
let isLoading = false
let awaitList = [];
async function updateCurrencies() {
try {
if (isLoading) return
isLoading = true
const res = await gmRequest("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/eur.json", {
responseType: "json"
});
const data = await res.json()
currencies.data = data.eur;
currencies.day = new Date().getDay();
awaitList.forEach(item => {
const span = container.querySelector(`#${item.id}`)
if (span) {
span.innerText = getExchangeRate(item.price, targetCurrency)
delete span.id
}
})
awaitList = [];
} catch (err) {
console.error(err)
awaitList.forEach(item => {
const span = container.querySelector(`#${item.id}`)
if (span) {
span.innerText = i18n("error")
span.title = err.message || err
delete span.id
}
})
awaitList = [];
} finally {
isLoading = false
}
}
function exchangeRate(price) {
const span = document.createElement("span");
if (!currencies.data || new Date().getDay() !== currencies.day) {
updateCurrencies();
span.innerText = i18n("waiting");
span.id = `a${new Date().getTime()}`
awaitList.push({
id: span.id,
price
});
} else {
span.innerText = getExchangeRate(price, targetCurrency);
}
return span.outerHTML;
}
function getExchangeRate(price, targetCurrency) {
const rate = currencies.data[targetCurrency.toLowerCase()] / currencies.data["jpy"];
return formatted((price * rate).toFixed(2));
}
function i18n(key, lang = localLang) {
if (i18nMap?.[lang]?.[key]) {
return i18nMap[lang][key]
}
return key
}
function updateInfo(userInfo) {
container.innerHTML = `<table style="background: transparent" class="table is-narrow is-fullwidth">
<thead>
<tr>
<th colspan="2">
<small title="${userInfo.acceptable ? '' : i18n("title")}">${i18n("price")}${userInfo.acceptable ? "" : "(" + i18n("referenceOnly") + ")"}</small>
</th>
</tr>
</thead>
<tbody>
${userInfo.skills.map((el) => {
return `<tr>
<td><small>${i18n(el.genre)}</small></td>
<td><small>JPY ${formatted(el.default_amount, "ja")} ~ <span id="">${targetCurrency.toUpperCase()} ${exchangeRate(el.default_amount)}</span></small></td>
</tr>`
}).join("")
}
</tbody>
</table>`
}
(async function () {
'use strict';
exchangeRate()
listenAjax(/api\/users\/[^\/]+\/works\/\d+$/, async (res) => {
container.innerHTML = i18n("loading");
const pathname = location.pathname
const userinfo = await (await fetch(res.url.split("/works")[0], {
headers: {
authorization: `Bearer ${localStorage.getItem("token")}`
}
})).json()
updateInfo(userinfo)
})
listenAjax(/api\/users\/[^\/]+$/, async (res) => {
container.innerHTML = i18n("loading");
updateInfo(res.res)
})
})()