// ==UserScript==
// @name Boothの購入履歴から累計散財額を計算するツール
// @namespace https://x.com/zerukuVRC
// @version 1.1
// @description お手軽鬱ボタン。Boothの購入履歴の総額を計算できます。同じセッションだけしか計算結果は保存できません。
// @author zeruku
// @match https://accounts.booth.pm/orders*
// @grant none
// @license MIT
// ==/UserScript==
// コードがながーい!
// ٩(๑`^´๑)۶
(function() {
/// ///
/// *===== URLパラメーターの初期設定 =====* ///
/// ///
var currentURL = new URL(window.location.href);
var autoCalculate = currentURL.searchParams.get('auto') === '1';
var changed = false;
if (currentURL.searchParams.get('total') === null) {
currentURL.searchParams.set('total', 0);
changed = true;
}
if (currentURL.searchParams.get('auto') === null) {
currentURL.searchParams.set('auto', autoCalculate ? '1' : '0');
changed = true;
}
if (currentURL.searchParams.get('page') === null) {
currentURL.searchParams.set('page', currentURL.searchParams.get('page') ?
currentURL.searchParams.get('page') : '1')
changed = true;
}
if (changed) {
window.location.href = currentURL.href;
}
/// ///
/// *===== プログレス表示部分の定義 =====* ///
/// ///
var progressText = document.createElement("div");
progressText.style.position = "fixed";
progressText.style.bottom = "40px";
progressText.style.left = "10px";
progressText.style.color = "#fc4d50";
// ʕ´•ﻌ•`ʔ.。o(これいる?よくわかんない)
progressText.style.zIndex = "1000";
/// ///
/// *===== 商品のID、バリエーションを取得 =====* ///
/// ///
/// Tips: バリエーションを取得して ///
/// 後の関数で購入したバリエーションを確定させて ///
/// 値段を決定する ///
/// ///
function collectItemInfo(item_element) {
var url = item_element.firstChild.href;
var item_id = url.match(/\d+$/g);
var item_variation = (item_element.parentElement.children[1].children[1].innerText
.match(/\(.*\)$/g) || [null])[0];
if (item_variation) {
item_variation = item_variation.replace(/^\(/, "").replace(/\)$/, "");
}
return {
item_id: item_id[0],
item_variation: item_variation
};
}
/// ///
/// *===== 商品の価格を取得 =====* ///
/// ///
function fetchItemPrice(item_id, item_variation_name) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', "https://booth.pm/ja/items/" + item_id + ".json", true);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var response = JSON.parse(xhr.responseText);
var variations = response.variations;
var item_price = variations.find(variation => variation.name === // 上のTipsで書いた部分
item_variation_name);
if (item_price) {
resolve({
item_name: response.name,
item_price: item_price.price
});
} else {
resolve({
item_name: response.name,
item_price: variations[0].price
});
}
} else if (xhr.status === 404) {
resolve("商品が削除されたか、非公開にされています");
} else {
reject("リクエストエラー: ステータスコード " + xhr.status);
}
}
};
xhr.send();
});
}
/// ///
/// *===== 自動化の際、次のページにリダイレクトさせる関数 =====* ///
/// ///
function processPageParamAndRedirect(url) {
var parsedURL = new URL(url);
var pageParam = parsedURL.searchParams.get('page');
var currentPage = parseInt(pageParam, 10);
var nextPage = currentPage + 1;
parsedURL.searchParams.set('page', nextPage.toString());
window.location.href = parsedURL.href;
}
/// ///
/// *===== 自動化の際、終了したら1ページ目に転送 =====* ///
/// ///
function pageReset() {
var parsedURL = new URL(window.location.href);
parsedURL.searchParams.set('page', '1');
parsedURL.searchParams.set('auto', '0');
window.location.href = parsedURL.href;
}
/// ///
/// *===== メイン関数 =====* ///
/// ///
var item_list = [];
async function main() {
// 計算を開始したら、ボタンを無効化する
var button = document.querySelector(".booth-total-price-button");
button.disabled = true;
button.style.cursor = "wait";
// 購入履歴ページの購入したアイテムを取得
var item_list_elements = Array.from(document.querySelectorAll(
'[class="l-col-auto"]'));
item_list = item_list_elements.map(collectItemInfo);
// もし最後のページで、商品が一つもなかったら終了
if (item_list.length === 0) {
alert("計算が終了しました");
pageReset();
}
// 変数定義
var price_list = [];
var totalItems = item_list.length;
var completedItems = 0;
// プログレス表示
progressText.textContent = `計算中... : ${completedItems}/${totalItems}`;
document.body.appendChild(progressText);
for (let i = 0; i < item_list.length; i++) {
// 変数定義
var item_info = item_list[i];
var item_id = item_info.item_id;
var item_variation_name = item_info.item_variation;
try {
// アイテムの価格を取得
var item_price = await fetchItemPrice(item_id, item_variation_name);
price_list.push(item_price.item_price);
} catch (error) {
console.error(error);
}
completedItems++;
// プログレス表示
progressText.textContent = `進行中: ${completedItems}/${totalItems}`;
/// **--- 重要! --- ** ///
/// この遅延は、Boothのサーバーに負荷をかけないために存在します ///
/// 意図的に時間を変えたり、削除して処理速度を早めるのは禁止です ///
await new Promise(resolve => setTimeout(resolve, 900));
}
// 価格の合計を計算
price_list = price_list.filter(element => !(element == undefined));
var total_price = price_list.reduce(function(a, b) {
return a + b;
});
// totalパラメーターに合計金額を追加
var url = new URL(window.location.href);
var existingTotal = parseFloat(url.searchParams.get('total')) || 0;
var newTotal = existingTotal + total_price;
url.searchParams.set('total', newTotal);
if (!autoCalculate)
// 自動化が無効の場合
{
// アラートを表示
alert(`このページの合計金額: ${total_price}円\n今までの合計金額: ${newTotal}円`)
window.location.href = url.href;
// 一応二度押せないようにする
progressText.textContent = "";
button.disabled = true;
button.textContent = "計算済み"
}
else
// 自動化が有効の場合
{
// 次のページにそのまま転送、アラートを表示しない
processPageParamAndRedirect(url)
}
}
// 合計金額をtotalパラメーターから取得
// TODO: まだ最適化の余地あり?面倒くさいかも ꜀( ꜆ᐢ. ̫.ᐢ)꜆ パタ
var total_price = new URL(window.location.href).searchParams.get("total");
/// ///
/// *===== 金額計算ボタンの表示 =====* ///
/// ///
function addButton() {
const button = document.createElement("button");
button.innerText = "金額計算";
button.classList.add("booth-total-price-button");
button.style.background = "#fc4d50";
button.style.color = '#ffffff';
button.style.borderRadius = '20px';
button.style.padding = '10px 15px';
button.style.border = 'none';
button.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)';
button.style.cursor = 'pointer';
button.style.position = 'fixed';
button.style.bottom = '10px';
button.style.left = '10px';
button.style.zIndex = '1000';
button.addEventListener('mouseover', () => {
button.style.background = '#ff6669';
});
button.addEventListener('mouseout', () => {
button.style.background = '#fc4d50';
});
button.onclick = main;
document.body.appendChild(button);
}
addButton();
/// ///
/// *===== 数値リセットの関数 =====* ///
/// *===== リセットボタンの表示 =====* ///
/// ///
function resetTotal() {
var url = new URL(window.location.href);
url.searchParams.set('total', 0);
url.searchParams.set('auto', 0)
if (!confirm("累計金額をリセットしますか?")) {
return;
}
window.location.href = url.href;
}
function addResetButton() {
const resetButton = document.createElement("button");
resetButton.innerText = "累計金額をリセット";
resetButton.classList.add("booth-reset-button");
resetButton.style.background = "#0077B5";
resetButton.style.color = '#ffffff';
resetButton.style.borderRadius = '20px';
resetButton.style.padding = '10px 15px';
resetButton.style.border = 'none';
resetButton.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)';
resetButton.style.cursor = 'pointer';
resetButton.style.position = 'fixed';
resetButton.style.bottom = '10px';
resetButton.style.left = '280px';
resetButton.style.zIndex = '1000';
resetButton.addEventListener('mouseover', () => {
resetButton.style.background = '#005588';
});
resetButton.addEventListener('mouseout', () => {
resetButton.style.background = '#0077B5';
});
resetButton.onclick = resetTotal;
document.body.appendChild(resetButton);
}
addResetButton();
/// ///
/// *===== ツイートボタンの表示 =====* ///
/// ///
function addTweetButton(total_price) {
const tweetButton = document.createElement("button");
tweetButton.innerText = "Twitterに共有";
tweetButton.style.background = "#1DA1F2";
tweetButton.style.color = "#fff";
tweetButton.style.borderRadius = "20px";
tweetButton.style.padding = "10px 15px";
tweetButton.style.border = "none";
tweetButton.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)";
tweetButton.style.cursor = "pointer";
tweetButton.style.position = "fixed";
tweetButton.style.bottom = "55px";
tweetButton.style.left = "280px";
tweetButton.style.zIndex = "1000";
tweetButton.addEventListener("mouseover", () => {
tweetButton.style.background = "#1A91DA";
});
tweetButton.addEventListener("mouseout", () => {
tweetButton.style.background = "#1DA1F2";
});
tweetButton.onclick = function() {
const tweetText =
`私がBoothで使用した合計金額は、『${Number(total_price).toLocaleString()}円』でした!\n\n#私がBoothに使った金額`;
// ( ´~`).。 (いずれx.comにしないといけないのかな...?)
const tweetURL = "https://twitter.com/intent/tweet?text=" +
encodeURIComponent(tweetText);
window.open(tweetURL, "_blank");
};
document.body.appendChild(tweetButton);
}
addTweetButton(total_price);
/// ///
/// *===== 自動化の開始 =====* ///
/// ただのパラメーター変更! ///
/// ///
function startAuto() {
var url = new URL(window.location.href);
url.searchParams.set('auto', 1);
window.location.href = url.href;
}
/// ///
/// *===== 自動化の停止 =====* ///
/// ただのパラメーター変更! ///
/// ///
function stopAuto() {
var url = new URL(window.location.href);
url.searchParams.set('auto', 0);
window.location.href = url.href;
};
/// ///
/// *===== 自動化ボタンの表示 =====* ///
/// ///
function addAutoButton() {
const autoButton = document.createElement("button");
autoButton.innerText = "自動計算開始!";
autoButton.style.background = "#1b7f8c";
autoButton.style.color = "#fff";
autoButton.style.borderRadius = "20px";
autoButton.style.padding = "10px 15px";
autoButton.style.border = "none";
autoButton.style.boxShadow = "0 2px 4px rgba(0, 0, 0, 0.1)";
autoButton.style.cursor = "pointer";
autoButton.style.position = "fixed";
autoButton.style.bottom = "10px";
autoButton.style.left = "120px";
autoButton.style.zIndex = "1000";
if (autoCalculate) {
autoButton.addEventListener("mouseover", () => {
autoButton.style.background = "#c00b3c";
});
autoButton.addEventListener("mouseout", () => {
autoButton.style.background = "#f30f4c";
});
autoButton.innerText = "自動計算を停止";
autoButton.style.background = "#f30f4c";
autoButton.onclick = stopAuto;
} else {
autoButton.addEventListener("mouseover", () => {
autoButton.style.background = "#22a1b2";
});
autoButton.addEventListener("mouseout", () => {
autoButton.style.background = "#1b7f8c";
});
autoButton.onclick = startAuto;
}
document.body.appendChild(autoButton);
}
addAutoButton();
/// ///
/// *===== 累計金額の表示 =====* ///
/// ///
function setTotalPrice(total_price) {
var totalText = document.createElement("div");
totalText.style.position = "fixed";
totalText.style.bottom = "55px";
totalText.style.left = "10px";
totalText.style.backgroundColor = "#333";
totalText.style.color = "#fff";
totalText.style.padding = "6px 18px";
totalText.style.borderRadius = "20px";
totalText.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1';
totalText.style.border = 'none';
totalText.style.zIndex = "1000";
totalText.textContent = "累計金額: " + Number(total_price).toLocaleString() + "円";
document.body.appendChild(totalText);
}
setTotalPrice(total_price);
/// ///
/// *===== スタート!!! =====* ///
/// - ̗̀ ( ˶'ᵕ'˶) ̖́- ///
if (autoCalculate) {
main()
}
})();