Skeb enhance

Added currency conversion functionality.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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