Skeb enhance

Supports multi-currency calculation to optimize user experience

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Skeb enhance
// @name:zh-CN   Skeb 增强
// @name:ja-JP   Skeb強化
// @name:en-US   Skeb enhance
// @namespace    http://tampermonkey.net/
// @version      2026-05-16
// @description  Supports multi-currency calculation to optimize user experience
// @description:zh-CN  支持计算多国汇率优化使用体验
// @description:ja-JP  複数通貨の換算に対応し、使用体験を最適化
// @description:en-US  Supports multi-currency calculation to optimize user experience
// @author       xiyuesaves
// @license      AGPLv3
// @match        https://skeb.jp/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=skeb.jp
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      cdn.jsdelivr.net
// ==/UserScript==

const isDebug = true
const langCode = (navigator.language || "en").split('-')[0].toLowerCase();
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": "Genre",
    "novel": "Novel",
    "video": "Video",
    "voice": "Voice",
    "correction": "Suggestion",
    "title": "Author can modify the price before the commission opens",
    "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 localLang = i18nMap[langCode] ? langCode : "en";
const targetCurrency = i18nMap[localLang]?.currency || i18nMap["en"].currency;
const container = createElement("skeb-enhanced-container");
const wrapper = createElement("skeb-enhanced-wrapper");
const ajaxList = [];
const listenList = [];
const usersMap = getUserMap()
const currencies = {
  day: 0,
  data: null
};
const log = (...args) => isDebug && console.log(...args);
const originOpen = XMLHttpRequest.prototype.open;
const originSend = XMLHttpRequest.prototype.send;
const originHeader = XMLHttpRequest.prototype.setRequestHeader;

let isLoading = false
let awaitList = [];

XMLHttpRequest.prototype.open = function () {
  this.addEventListener("load", function (obj) {
    const url = obj.target.responseURL;
    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 createElement(className, tagName = "div") {
  const element = document.createElement("div")
  element.className = className
  return element;
}

function insertContainer() {
  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"
  const timer = setInterval(() => {
    if (document.querySelector(".skeb-enhanced")) {
      clearInterval(timer)
      return;
    }
    const divider = document.querySelector(firstSelector)
    if (divider) {
      divider.insertAdjacentElement("afterend", container)
      clearInterval(timer)
      return;
    }
    const table = document.querySelector(scondSelector)
    if (table) {
      table.insertAdjacentElement("beforebegin", container)
      clearInterval(timer)
    }
  }, 100)
}

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}`)),
    });
  });
}

async function updateCurrencies() {
  try {
    if (isLoading) return
    isLoading = true
    const res = await gmRequest("https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/jpy.json", {
      responseType: "json"
    });
    const data = await res.json()

    currencies.data = data.jpy;
    currencies.day = new Date().getDay();

    log("updateCurrencies", currencies.data)

    awaitList.forEach(item => {
      const span = document.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 = document.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) {
    span.innerText = i18n("waiting");
    span.id = `id-${crypto.randomUUID()}`
    awaitList.push({
      id: span.id,
      price
    });
    updateCurrencies();
  } else {
    span.innerText = getExchangeRate(price, targetCurrency);
  }
  return span.outerHTML;
}

function getExchangeRate(price, targetCurrency) {
  const rate = currencies.data[targetCurrency.toLowerCase()];
  const convertedAmount = rate * price;
  return formatted(convertedAmount.toFixed(2));
}

function i18n(key, lang = localLang) {
  if (i18nMap?.[lang]?.[key]) {
    return i18nMap[lang][key]
  }
  return key
}

function updateInfo(userInfo) {
  container.innerHTML = "";
  container.insertAdjacentElement("afterbegin",
    html`
<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>
        ${createSkillsList(userInfo)}
    </tbody>
</table>`)
}

function createSkillsList(userInfo) {
  return userInfo.skills.map(el => {
    const amountStr = `JPY ${formatted(el.default_amount, "ja")}` +
      (localLang !== 'ja'
        ? ` ~ ${targetCurrency.toUpperCase()} ${exchangeRate(el.default_amount)}`
        : '');
    return `<tr>
                <td><small>${i18n(el.genre)}</small></td>
                <td><small>${amountStr}</small></td>
            </tr>`;
  }).join("")
}

async function getUserInfo(userId) {
  let data = usersMap.get(userId)
  log(data)
  if (!data || new Date().getTime() - (data?.time ?? 0) > 24 * 60 * 60 * 1000) {
    userInfo = await (await fetch(`/api/users/${userId}`, {
      headers: {
        authorization: `Bearer ${localStorage.getItem("token")} `
      }
    })).json()
    data = {
      userInfo,
      time: new Date().getTime()
    }
  }

  usersMap.set(userId, data)
  updateUserMap()
  return data.userInfo
}

function updateUserMap() {
  localStorage.setItem("usersMap", JSON.stringify(Array.from(usersMap)))
}

function getUserMap() {
  try {
    const localMap = new Map(JSON.parse(localStorage.getItem("usersMap") || "[]"))
    const newMap = new Map()
    localMap.forEach((value, key) => {
      if (new Date().getTime() - value.time < 24 * 60 * 60 * 1000) {
        newMap.set(key, value)
      }
    })
    return localMap
  } catch (e) {
    return new Map()
  }
}

function initStyle() {
  GM_addStyle(css`
.skeb-enhanced-container {
    border: solid 1px #cbd0d7;
    border-radius: 8px;
    background: #e4e6ea;
    box-sizing: border-box;
    padding: 8px;
    width: 100%;
    display: flex;
    justify-content: space-between;
    height: auto;
    margin-bottom: 1.5rem;
}`);
}

function html(strings, ...values) {
  let result = "";
  for (let i = 0; i < strings.length; i++) {
    result += strings[i];
    if (i < values.length) {
      result += values[i];
    }
  }
  const domParser = new DOMParser().parseFromString(result, "text/html");
  const element = domParser.body;
  return element;
}

function css(strings, ...values) {
  let result = "";
  for (let i = 0; i < strings.length; i++) {
    result += strings[i];
    if (i < values.length) {
      result += values[i];
    }
  }
  return result;
}

(async function () {
  'use strict';

  initStyle();

  listenAjax(/api\/users\/[^\/]+\/works\/\d+$/, async (args) => {
    log("api/users/works", args)
    insertContainer();
    container.innerHTML = i18n("loading");
    const pathname = location.pathname
    const userInfo = await getUserInfo(args.res.creator.screen_name)
    usersMap.set(userInfo.screen_name, {
      userInfo,
      time: new Date().getTime()
    })
    updateUserMap()
    updateInfo(userInfo)
  })

  listenAjax(/api\/users\/[^\/]+$/, async (args) => {
    log("api/users", args)
    insertContainer();
    container.innerHTML = i18n("loading");
    usersMap.set(args.res.screen_name, {
      userInfo: args.res,
      time: new Date().getTime()
    })
    updateUserMap()
    updateInfo(args.res)
  })
})()