Find_Extra_card

查找徽章满级但是仍然有卡牌的游戏

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name            Find_Extra_card
// @name:zh-CN      Steam寻找多余的卡牌
// @namespace       https://blog.chrxw.com
// @version	        1.8
// @description	    查找徽章满级但是仍然有卡牌的游戏
// @description:zh-CN  查找徽章满级但是仍然有卡牌的游戏
// @author          Chr_
// @include         /https://steamcommunity\.com/(id|profiles)/[^\/]+/badges/?(\/$|\/\?)?/
// @supportURL      https://steamcn.com/t339531-1-1
// @license         AGPL-3.0
// @icon            https://blog.chrxw.com/favicon.ico
// @grant           GM_addStyle
// @grant           GM_setClipboard
// ==/UserScript==

(() => {
    "use strict";
    const WorkTread = 5; // 抓取线程
    const SleepTime = 50; // 抓取间隔

    const { origin, pathname } = window.location;
    const MatchAppId = /gamecards\/(\d+)/;
    const Line = "==============================\n";

    let isWorking = false;

    init();
    //初始化
    function init() {
        const genBtn = (text, onclick) => {
            const btn = document.createElement("button");
            btn.textContent = text;
            btn.className = "btn_medium fec_btn";
            btn.addEventListener("click", onclick);
            return btn;
        };
        const bar = document.querySelector(".profile_small_header_text");
        const btnHelp = genBtn("❔说明", () => {
            const { script: { version } } = GM_info;
            ShowAlertDialog(``, [
                `<h2>【插件版本 ${version}】</h2>`,
                `<p>【📇查找本页】:查找当前页面, 徽章已经满级(5级), 但是库中仍然有多余卡牌的游戏</p>`,
                `<p>【📇查找全部】:查找所有徽章, 徽章已经满级(5级), 但是库中仍然有多余卡牌的游戏</p>`,
                `<p>【<a href="https://keylol.com/t772471-1-1" target="_blank">发布帖</a>】 【<a href="https://blog.chrxw.com/scripts.html" target="_blank">脚本反馈</a>】 【Developed by <a href="https://steamcommunity.com/id/Chr_" target="_blank">Chr_</a>】</p>`
            ].join(""));
        });
        const btnFindAll = genBtn("📇查找全部", findAllExtraCard);
        const btnFindOne = genBtn("📇查找本页", findCurrExtraCard);
        bar.appendChild(btnHelp);
        bar.appendChild(btnFindAll);
        bar.appendChild(btnFindOne);
    }
    //读取当前页
    async function findCurrExtraCard() {
        const [title, text, btnAbort] = showDialog();

        isWorking = true;
        btnAbort.disabled = false;
        title.innerText = "读取本页徽章信息";
        text.value += `开始运行 线程数量:${WorkTread}\n${Line}【AppId】 | 【持有】/【一套】 | 【游戏名】\n` + Line;

        const box = document.querySelector(".maincontent>.badges_sheet");
        if (box !== null) {
            const badges = parseDom2BadgeList(box);
            let count = 0;
            if (badges.length === 0) {
                text.value += "没有找到任何满级徽章\n";
            } else {
                title.innerText = `运行进度 【 0 / ${badges.length} 】`;
                for (let i = 0; i < badges.length && isWorking; i += WorkTread) {
                    const max = Math.min(i + WorkTread, badges.length);
                    const tasks = [];
                    for (let j = i; j < max; j++) {
                        const [url, name] = badges[j];
                        tasks.push(getCardInfo(url, name));
                    }
                    const values = await Promise.all(tasks);

                    for (const [succ, appId, name, sum, total] of values) {
                        if (succ && sum > 0) {
                            count++;
                            text.value += `${appId.padEnd(7)} | ${sum} / ${total} | ${name}\n`;
                        }
                    }
                    title.innerText = `运行进度 【 ${max} / ${badges.length} 】`;
                    await aiosleep(SleepTime);
                }
            }
            text.value += Line + `共找到 ${count} 个徽章满级但仍有剩余卡牌的游戏\n`;
        } else {
            text.value += Line + "没有找到任何徽章\n";
        }
        isWorking = false;
        title.innerText = "运行结束";
        btnAbort.disabled = true;
    }
    //读取全部
    async function findAllExtraCard() {
        const [title, text, btnAbort] = showDialog();
        isWorking = true;
        btnAbort.disabled = false;
        title.innerText = "读取全部徽章信息";
        text.value += `开始运行 线程数量:${WorkTread}\n${Line}【AppId】 | 【持有】/【一套】 | 【游戏名】\n` + Line;

        let count = 0;
        let page = 1;
        while (isWorking) {
            const ele = await getBadgeList(page++);

            if(ele===null){
                continue;
            }

            const box = ele.querySelector(".maincontent>.badges_sheet");
            if (box !== null) {
                const badges = parseDom2BadgeList(box);
                if (badges === null) {
                    break;
                }
                if (badges.length > 0) {
                    title.innerText = `运行进度 第【${page - 1}】页 【 0 / ${badges.length} 】`;
                    for (let i = 0; i < badges.length && isWorking; i += WorkTread) {
                        const max = Math.min(i + WorkTread, badges.length);
                        const tasks = [];
                        for (let j = i; j < max; j++) {
                            const [url, name] = badges[j];
                            tasks.push(getCardInfo(url, name));
                        }
                        const values = await Promise.all(tasks);

                        for (const [succ, appId, name, sum, total] of values) {
                            if (succ && sum > 0) {
                                count++;
                                text.value += `${appId.padEnd(7)} | ${sum} / ${total} | ${name}\n`;
                            }
                        }
                        title.innerText = `运行进度 第【${page - 1}】页 【 ${max} / ${badges.length} 】`;
                        await aiosleep(SleepTime);
                    }
                }
            }
        }

        if (count == 0) {
            text.value += Line + "没有找到任何徽章\n";
        } else {
            text.value += Line + `共找到 ${count} 个徽章满级但仍有剩余卡牌的游戏\n`;
        }

        isWorking = false;
        title.innerText = "运行结束";
        btnAbort.disabled = true;
    }

    //显示提示框
    function showDialog() {
        const genBtn = (text, onclick) => {
            const btn = document.createElement("button");
            btn.textContent = text;
            btn.className = "btn_medium fec_btn";
            if (onclick) { btn.addEventListener("click", onclick); }
            return btn;
        };
        const area = document.createElement("div");
        area.className = "fec_area";
        const tit = document.createElement("h2");
        tit.className = "fec_title";
        tit.innerText = "";
        const txt = document.createElement("textarea");
        txt.className = "fec_text";
        const action = document.createElement("div");
        action.className = "fec_action";
        const btnAbort = genBtn("⛔停止运行", () => {
            if (isWorking) {
                isWorking = false;
                tit.innerText = "已停止";
            }
        });
        btnAbort.disabled = true;
        const btnClose = genBtn("❌关闭", null);
        const btnCopy = genBtn("📋复制", () => {
            GM_setClipboard(txt.value, "text");
            btnCopy.innerText = "✅已复制";
            setTimeout(() => { btnCopy.innerText = "📋复制"; }, 1000);
        });
        action.appendChild(btnCopy);
        action.appendChild(btnAbort);
        action.appendChild(btnClose);
        area.appendChild(tit);
        area.appendChild(txt);
        area.appendChild(action);
        const diag = ShowDialog("", area, { bExplicitDismissalOnly: true });
        btnClose.addEventListener("click", () => { diag.Dismiss(); });
        return [tit, txt, btnAbort];
    }
    //异步Sleep
    function aiosleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    //解析徽章列表的DOM节点
    function parseDom2BadgeList(ele) {
        const badges = ele.querySelectorAll(".badge_row.is_link");
        if (badges.length === 0) {
            return null;
        }

        const maxBadges = [];
        for (const badge of badges) {
            const url = badge.querySelector("a.badge_row_overlay")?.href;
            const level = badge.querySelector(".badge_info_description>div:nth-child(2)")?.innerText.trim() ?? "0 级";
            const title = badge.querySelector(".badge_title")?.innerText?.trim()?.split("\t")[0] ?? "Null";
            if (url && level && level.startsWith("5 级")) {
                maxBadges.push([url, title]);
            }
        }
        return maxBadges;
    }

    //读取卡牌页面
    function getCardInfo(url, title) {
        const matchUrl = url.match(MatchAppId);
        return new Promise((resolve, reject) => {
            fetch(url)
                .then(res => res.text())
                .then(html => {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(html, 'text/html');

                    const cardCount = doc.querySelectorAll(".badge_card_set_text_qty");
                    const cardTotal = cardCount.length;

                    if (cardTotal === 0) { resolve([true, matchUrl[1], title, 0, 0]); }

                    let sum = 0;
                    for (let i = 0; i < cardTotal; i++) {
                        let text = cardCount[i].innerText;
                        let num = text.substring(1, text.length - 1);
                        try {
                            sum += parseInt(num);
                        } catch (e) {
                            console.error(e);
                        }
                    }

                    resolve([true, matchUrl[1], title, sum, cardTotal]);
                })
                .catch(err => {
                    console.error("请求失败", err);
                    resolve([false, matchUrl[1], null, null, null]);
                });
        });
    }
    //读取徽章页面
    async function getBadgeList(page) {
        return new Promise((resolve, reject) => {
            fetch(`${origin}${pathname}?sort=c&p=${page}&l=schinese`)
                .then(res => res.text())
                .then(html => {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(html, 'text/html');
                    resolve(doc);
                })
                .catch(err => {
                    console.error("请求失败", err);
                    resolve(null);
                });
        });
    }

})();

GM_addStyle(`
.profile_small_header_text > .fec_btn {
    float: right;
  }
  .profile_small_header_text > .fec_btn {
    margin-left: 5px;
  }
  .fec_action > .fec_btn:not(:first-child) {
    margin-left: 20px;
  }
  .fec_btn {
    padding: 3px 6px;
  }
  .fec_action {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    margin-top: 5px;
  }
  .fec_action > .fec_btn {
    flex: 0 0 auto;
  }
  .fec_text {
    height: 300px;
    width: 600px;
    resize: vertical;
    font-size: 15px;
    margin: 5px 0;
    padding: 5px;
    background-color: rgba(0, 0, 0, 0.4);
    color: #fff;
    border: 1 px solid #000;
    border-radius: 3 px;
    box-shadow: 1px 1px 0px #45556c;
  }
  .fec_area {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    width: 100%;
    height: 100%;
  }
  .fec_area > * {
    width: 100%;
  }
  
`);