Skeb enhance

Added currency conversion functionality.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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