Skeb enhance

Added currency conversion functionality.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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