Boosterpack_Enhance

补充包制作工具

// ==UserScript==
// @name:zh-CN   补充包合成器增强
// @name         Boosterpack_Enhance
// @namespace    https://blog.chrxw.com
// @version      1.4
// @description  补充包制作工具
// @description:zh-CN  补充包制作工具
// @author       Chr_
// @match        https://steamcommunity.com/tradingcards/boostercreator*
// @match        https://steamcommunity.com//tradingcards/boostercreator/*
// @license      AGPL-3.0
// @icon         https://blog.chrxw.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @require      https://cdnjs.cloudflare.com/ajax/libs/tabulator/6.3.0/js/tabulator.min.js
// @resource     css https://cdnjs.cloudflare.com/ajax/libs/tabulator/6.3.0/css/tabulator_midnight.min.css
// ==/UserScript==

(() => {
    'use strict';

    const g_boosterData = {};
    const g_faveriteBooster = new Set();
    let g_craftMode = "2";

    // 初始化
    setTimeout(async () => {
        loadFavorite();
        await initBoosterData();
        initPanel();
    }, 200);

    function genDiv(cls) {
        const d = document.createElement("div");
        d.className = cls;
        return d;
    }
    function genInput(cls) {
        const i = document.createElement("input");
        i.className = cls;
        return i;
    }
    function genSpan(name) {
        const s = document.createElement("span");
        s.textContent = name;
        return s;
    }
    function genCheckbox(name, cls, key = null, checked = false) {
        const l = document.createElement("label");
        const i = document.createElement("input");
        const s = genSpan(name);
        i.textContent = name;
        i.title = name;
        i.type = "checkbox";
        i.className = "fac_checkbox";
        i.checked = localStorage.getItem(key) === "true";
        l.title = name;
        l.appendChild(i);
        l.appendChild(s);
        return [l, i];
    }
    function genImage(url, cls = "bh-image") {
        const i = document.createElement("img");
        i.src = url;
        i.className = cls;
        return i;
    }
    function genButton(name, foo, cls = "bh-button") {
        const b = document.createElement("button");
        b.textContent = name;
        b.title = name;
        b.className = cls;
        b.addEventListener("click", foo);
        return b;
    }
    function genOption(name, value) {
        const o = document.createElement("option");
        o.textContent = name;
        o.value = value;
        return o;
    }
    function genSelector() {
        const s = document.createElement("select");
        s.appendChild(genOption("可交易", "2"));
        s.appendChild(genOption("不可交易", "3"));
        return s;
    }

    function initPanel() {
        const area = document.querySelector("div.booster_creator_area");

        const filterContainer = genDiv("bh-filter");
        area.appendChild(filterContainer);

        const iptSearch = genInput("bh-search");
        iptSearch.placeholder = "搜索名称 / AppId";
        let t = 0;
        iptSearch.addEventListener("keydown", () => {
            clearTimeout(t);
            t = setTimeout(updateFilter, 500);
        });
        filterContainer.appendChild(iptSearch);

        const [lblOnlyFavorite, chkOnlyFavorite] = genCheckbox("仅显示已收藏", "bh-checkbox", "bh-onlyfavorite", false);
        chkOnlyFavorite.addEventListener("change", updateFilter);
        filterContainer.appendChild(lblOnlyFavorite);
        const [lblOnlyCraftable, chkOnlyCraftable] = genCheckbox("仅显示可合成", "bh-checkbox", "bh-onlycraftable", false);
        chkOnlyCraftable.addEventListener("change", updateFilter);
        filterContainer.appendChild(lblOnlyCraftable);

        const btnSearch = genButton("清除过滤条件", () => {
            iptSearch.value = "";
            chkOnlyFavorite.checked = false;
            chkOnlyCraftable.checked = false;
            updateFilter();
        }, "bh-button");
        filterContainer.appendChild(btnSearch);
        filterContainer.appendChild(genSpan(""));

        const tabledata = Object.values(g_boosterData);

        const divRight = genDiv("bh-right");
        filterContainer.appendChild(divRight);

        const selPackPrefer = genSelector();
        selPackPrefer.addEventListener("change", (_) => {
            g_craftMode = selPackPrefer.value;
            console.log(g_craftMode);
        })
        divRight.appendChild(selPackPrefer);

        const btnBatchCraft = genButton("批量合成收藏的包", async () => {
            const favoriteItems = tabledata.filter(x => x.favorite && x.available);
            if (favoriteItems.length === 0) {
                alert("无可合成项目");
            } else {
                for (let fav of favoriteItems) {
                    await doCraftBooster2(fav.appid, fav.contailer);
                    await asleep(200);
                }
            }
        }, "bh-button-right");
        divRight.appendChild(btnBatchCraft);

        const tableContainer = genDiv("bh-table");
        area.appendChild(tableContainer);

        const rowMenu = [
            {
                label: "收藏 / 取消收藏",
                action: doEditFavorite,
            }, {
                label: "合成补充包",
                action: doCraftBooster,
            },
        ];

        const table = new Tabulator(tableContainer, {
            height: 600,
            data: tabledata,
            layout: "fitDataStretch",
            rowHeight: 40,
            rowContextMenu: rowMenu,
            initialSort: [
                { column: "favorite", dir: "desc" },
            ],
            columns: [
                { title: "AppId", field: "appid" },
                { title: "图片", field: "appid", formatter: appImageFormatter, headerSort: false, width: 100, resizable: false },
                { title: "名称", field: "fullName", width: 300 },
                { title: "张数", field: "cardSet" },
                { title: "宝珠", field: "gemPrice" },
                {
                    title: "收藏",
                    field: "favorite",
                    formatter: "tickCross",
                    sorter: "boolean",
                    cellClick: (e, cell) => doEditFavorite(e, cell.getRow()),
                },
                { title: "合成", field: "available", formatter: "tickCross", sorter: "boolean" },
                { title: "操作", field: "available", frozen: true, formatter: operatorFormatter, headerSort: false },
            ],
        });

        window.addEventListener("hashchange", () => {
            const appId = location.hash.replace("#", "");
            iptSearch.value = appId;
            updateFilter();
        });

        window.addEventListener("beforeunload", () => {
            localStorage.setItem("bh-onlyfavorite", chkOnlyFavorite.checked);
            localStorage.setItem("bh-onlycraftable", chkOnlyCraftable.checked);
        });

        updateFilter();

        function doEditFavorite(e, row) {
            const cell = row.getCell("favorite");
            const newValue = !cell.getValue();
            cell.setValue(newValue);
            const appId = row.getCell("appid").getValue();
            const strAppId = `${appId}`;
            if (newValue) {
                g_faveriteBooster.add(strAppId);
            } else {
                g_faveriteBooster.delete(strAppId);
            }
            saveFavorite();
        }

        function doCraftBooster(e, row) {
            const appid = row.getCell("appid").getValue();
            const available = row.getCell("available").getValue();
            const container = row.getCell("container").getValue();
            if (available) {
                doCraftBooster2(appid, container);
            }
        }

        function updateFilter() {
            const filters = [{ field: "keywords", type: "like", value: iptSearch.value.trim(), matchAll: true }];
            if (chkOnlyFavorite.checked) {
                filters.push({ field: "favorite", type: "=", value: true });
            }
            if (chkOnlyCraftable.checked) {
                filters.push({ field: "available", type: "=", value: true });
            }
            table.setFilter(filters);
        }

        function appImageFormatter(cell, formatterParams, onRendered) {
            const appid = cell.getValue();
            const src = `https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/${appid}/capsule_231x87.jpg`;
            const image = genImage(src, "be-row-image");
            return image;
        };

        function operatorFormatter(cell, formatterParams, onRendered) {
            const data = cell.getRow().getData();
            return data.contailer;
        };
    };

    function doCraftBooster2(appid, contailer) {
        let btn = contailer.querySelector("button");
        if (btn) {
            btn.disabled = true;
        }
        craftBoosterpack(appid)
            .then((success) => {
                if (success) {
                    g_boosterData[appid].available = false;
                    contailer.innerHTML = "";
                    contailer.appendChild(genSpan("合成成功"));
                    btn = null;
                } else {
                    btn.textContent = "合成失败";
                }
            })
            .catch((err) => {
                console.error(err);
                btn.textContent = "合成失败";
            }).finally(() => {
                if (btn) {
                    btn.disabled = false;
                }
            });
    }

    // 读取补充包列表
    async function initBoosterData() {
        const gemPrice2SetCount = {
            1200: 5,
            1000: 6,
            857: 7,
            750: 8,
            667: 9,
            600: 10,
            545: 11,
            500: 12,
            462: 13,
            429: 14,
            400: 15
        };

        const currentData = parseBoosterData(document.body.innerHTML);

        const nameEnDict = {};
        if (g_strLanguage !== "english") {
            const html = await loadSecondLanguage("english");
            const secondData = parseBoosterData(html);

            for (const { appid, name } of secondData) {
                if (appid && name) {
                    nameEnDict[appid] = name;
                }
            }
        }

        for (const item of currentData) {
            const { appid, name, unavailable, price, series, available_at_time } = item;
            const intPrice = parseInt(price);
            if (appid && name && intPrice === intPrice) {
                const nameEn = nameEnDict[appid] ?? "";
                let fullName;
                let keywords;
                if (name === nameEn) {
                    fullName = name;
                    keywords = `${appid} ${name}`.toLowerCase();
                } else {
                    fullName = `${name} (${nameEn})`;
                    keywords = `${appid} ${name} ${nameEn}`.toLowerCase();
                }

                const cardSet = gemPrice2SetCount[intPrice] ?? 0;
                const favorite = g_faveriteBooster.has(`${appid}`);

                //生成按钮
                const contailer = genDiv();
                if (!available_at_time) {
                    const benCraft = genButton("合成补充包", (e) => doCraftBooster2(appid, contailer), "bh-button");
                    contailer.appendChild(benCraft);
                } else {
                    const time = genSpan(available_at_time);
                    time.className = "bh-tips";
                    contailer.appendChild(time);
                }

                g_boosterData[appid] = {
                    appid,
                    fullName,
                    gemPrice: intPrice,
                    keywords,
                    series,
                    cardSet,
                    available_at_time,
                    available: !unavailable,
                    favorite,
                    contailer,
                };
            }
        }
    }

    function parseBoosterData(html) {
        const matchJson = new RegExp(/CBoosterCreatorPage\.Init\(([\s\S]+}]),\s*parseFloat/);
        const result = html.match(matchJson);
        if (result) {
            const json = result[1];
            return JSON.parse(json);
        } else {
            return [];
        }
    }

    function loadFavorite() {
        const value = localStorage.getItem("be_faviorite") ?? "";
        const arr = value.split('|').filter(x => x);

        g_faveriteBooster.clear();
        for (const item of arr) {
            g_faveriteBooster.add(item);
        }
    }

    function saveFavorite() {
        const value = Array.from(g_faveriteBooster).join('|');
        console.log(g_faveriteBooster);
        localStorage.setItem("be_faviorite", value);
    }

    // 加载第二语言
    function loadSecondLanguage(lang) {
        return new Promise((resolve, reject) => {
            fetch(
                `https://steamcommunity.com/tradingcards/boostercreator/?l=${lang}`,
                {
                    method: "GET",
                    credentials: "include",
                })
                .then((response) => {
                    return response.text();
                })
                .then((text) => {
                    resolve(text);
                })
                .catch((err) => {
                    reject(err);
                });
        });
    }

    // 合成补充包
    function craftBoosterpack(appId) {
        return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append("sessionid", g_sessionID);
            formData.append("appid", appId);
            formData.append("series", "1");
            formData.append("tradability_preference", g_craftMode);

            fetch(
                "https://steamcommunity.com/tradingcards/ajaxcreatebooster/",
                {
                    method: "POST",
                    body: formData,
                    credentials: "include",
                })
                .then(response => {
                    return response.json();
                })
                .then((json) => {
                    if (json.purchase_result) {
                        const { success } = json.purchase_result;
                        resolve(success === 1);
                    }
                    resolve(false);
                })
                .catch((err) => {
                    reject(err);
                });
        });
    }

    //异步延时
    function asleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
})();

GM_addStyle(GM_getResourceText("css"));
GM_addStyle(`
img.be-row-image {
  width: 90px;
  height: auto;
}
div.bh-filter {
  padding: 10px 0;
}
div.bh-filter > * {
  margin-right: 10px;
}
div.bh-right {
  display: inline;
  position: absolute;
  right: 10px;
}
div.bh-right > * {
  margin-left: 10px;
}
span.bh-tips {
  font-size: 10px;
}
`);