Greasy Fork

Greasy Fork is available in English.

Steam快速添加购物车

超级方便的添加购物车体验, 不用跳转商店页, 附带导入导出购物车功能.

当前为 2025-07-12 提交的版本,查看 最新版本

// ==UserScript==
// @name:zh-CN      Steam快速添加购物车
// @name            Fast_Add_Cart
// @namespace       https://blog.chrxw.com
// @supportURL      https://blog.chrxw.com/scripts.html
// @contributionURL https://afdian.com/@chr233
// @version         4.8
// @description:zh-CN  超级方便的添加购物车体验, 不用跳转商店页, 附带导入导出购物车功能.
// @description     Add to cart without redirect to cart page, also provide import/export cart feature.
// @author          Chr_
// @match           https://store.steampowered.com/*
// @match           https://steamcommunity.com/*
// @license         AGPL-3.0
// @icon            https://blog.chrxw.com/favicon.ico
// @grant           GM_addStyle
// @grant           GM_setClipboard
// @grant           GM_setValue
// @grant           GM_getValue
// @grant           GM_registerMenuCommand
// ==/UserScript==

(async () => {
  "use strict";

  // 多语言
  const LANG = {
    ZH: {
      langName: "中文",
      changeLang: "修改插件语言",
      facInputBoxPlaceHolder:
        "一行一条, 自动忽略【#】后面的内容, 支持的格式如下: (自动保存)",
      storeLink: "商店链接",
      steamDBLink: "DB链接",
      import: "导入",
      importDesc: "从文本框批量添加购物车(从上到下导入)",
      importDesc2: "当前页面无法导入购物车",
      export: "导出",
      exportDesc: "将购物车内容导出至文本框",
      exportConfirm: "输入框中含有内容, 请选择操作?",
      exportConfirmReplace: "覆盖原有内容",
      exportConfirmAppend: "添加到最后",
      copy: "复制",
      copyDesc: "复制文本框中的内容",
      copyDone: "复制到剪贴板成功",
      reset: "清除",
      resetDesc: "清除文本框和已保存的数据",
      resetConfirm: "您确定要清除文本框和已保存的数据吗?",
      history: "历史",
      historyDesc: "查看购物车历史记录",
      reload: "刷新",
      reloadDesc: "重新读取保存的购物车内容",
      reloadConfirm: "您确定要重新读取保存的购物车数据吗?",
      goBack: "返回",
      goBackDesc: "返回你当前的购物车",
      clear: "清空购物车",
      clearDesc: "清空购物车",
      clearConfirm: "您确定要移除所有您购物车中的物品吗?",
      help: "帮助",
      helpDesc: "显示帮助",
      helpTitle: "插件版本",
      formatError: "格式有误",
      chooseSub: "请选择SUB",
      operation: "操作中……",
      operationDone: "操作完成",
      addCart: "添加购物车",
      addCartTips: "添加到购物车……",
      addCartErrorSubNotFount: "未识别到SubID",
      noSubDesc: "可能尚未发行或者是免费游戏",
      inCart: "在购物车中",
      importingTitle: "正在导入购物车……",
      add: "添加",
      toCart: "到购物车",
      tips: "提示",
      ok: "是",
      no: "否",
      fetchingSubs: "读取可用SUB",
      noSubFound: "未找到可用SUB",
      networkError: "网络错误",
      addCartSuccess: "添加购物车成功",
      addCartError: "添加购物车失败",
      networkRequestError: "网络请求失败",
      unknownError: "未知错误",
      unrecognizedResult: "返回了未知结果",
      batchExtract: "批量提取",
      batchExtractDone: "批量提取完成",
      batchDesc: "AppID已提取, 可以在购物车页批量导入",
      onlyOnsale: " 仅打折",
      onlyOnsaleDesc: "勾选后批量导入时仅导入正在打折的游戏.",
      onlyOnsaleDesc2: "勾选后批量导出时仅导出正在打折的游戏.",
      notOnSale: "尚未打折, 跳过",
      showUserCountry: "显示账号区域",
      yes: "是",
    },
    EN: {
      langName: "English",
      changeLang: "Change plugin language",
      facInputBoxPlaceHolder:
        "One line one item, ignore the content after #, support format: (auto save)",
      storeLink: "Store link",
      steamDBLink: "DB link",
      import: "Import",
      importDesc: "Batch add cart from textbox (from top to bottom)",
      importDesc2: "Current page can't import cart",
      export: "Export",
      exportDesc: "Export cart content to textbox",
      exportConfirm: "Textbox contains content, please choose operation?",
      exportConfirmReplace: "Replace original content",
      exportConfirmAppend: "Append to the end",
      copy: "Copy",
      copyDesc: "Copy textbox content",
      copyDone: "Copy to clipboard success",
      reset: "Reset",
      resetDesc: "Clear textbox and saved data",
      resetConfirm: "Are you sure to clear textbox and saved cart data?",
      history: "History",
      historyDesc: "View cart history",
      reload: "Reload",
      reloadDesc: "Reload saved cart date",
      reloadConfirm: "Are you sure to reload saved cart data?",
      goBack: "Back",
      goBackDesc: "Back to your cart",
      clear: "Clear",
      clearDesc: "Clear cart",
      clearConfirm: "Are you sure to remove all items in your cart?",
      help: "Help",
      helpDesc: "Show help",
      helpTitle: "Plugin Version",
      formatError: "Format error",
      chooseSub: "Please choose SUB",
      operation: "Operation in progress……",
      operationDone: "Operation done",
      addCart: "Add cart",
      addCartTips: "Adding to cart……",
      addCartErrorSubNotFount: "Unrecognized SubID",
      noSubDesc: "Maybe not released or free game",
      inCart: "In cart",
      importingTitle: "Importing cart……",
      add: "Add",
      toCart: "To cart",
      tips: "Tips",
      ok: "OK",
      no: "No",
      fetchingSubs: "Fetching available SUB",
      noSubFound: "No available SUB",
      networkError: "Network error",
      addCartSuccess: "Add cart success",
      addCartError: "Add cart failed",
      networkRequestError: "Network request failed",
      unknownError: "Unknown error",
      unrecognizedResult: "Returned unrecognized result",
      batchExtract: "Extract Items",
      batchExtractDone: "Batch Extract Done",
      batchDesc: "AppID list now saved, goto cart page to use batch import.",
      onlyOnsale: " Only on sale",
      onlyOnsaleDesc:
        "If checked, script will ignore games that is not on sale when import cart.",
      onlyOnsaleDesc2:
        "If checked, script will ignore games that is not on sale when export cart.",
      notOnSale: "Not on sale, skip",
      showUserCountry: "Show user country",
      yes: "Yes",
    },
  };

  // 判断语言
  let language = GM_getValue("lang", "ZH");
  if (!language in LANG) {
    language = "ZH";
    GM_setValue("lang", language);
  }
  // 获取翻译文本
  const t = (key) => LANG[language][key] || key;

  const showUserCountry = GM_getValue("show_user_country", false);

  {
    // 自动弹出提示
    const languageTips = GM_getValue("languageTips", true);
    if (languageTips && language === "ZH") {
      if (!document.querySelector("html").lang.startsWith("zh")) {
        ShowConfirmDialog(
          "tips",
          "Fast add cart now support English, switch?",
          "Using English",
          "Don't show again"
        )
          .done(() => {
            GM_setValue("lang", "EN");
            GM_setValue("languageTips", false);
            window.location.reload();
          })
          .fail((bool) => {
            if (bool) {
              showAlert(
                "",
                "You can switch the plugin's language using TamperMonkey's menu."
              );
              GM_setValue("languageTips", false);
            }
          });
      }
    }
    //注册菜单
    GM_registerMenuCommand(`${t("changeLang")} (${t("langName")})`, () => {
      switch (language) {
        case "EN":
          language = "ZH";
          break;
        case "ZH":
          language = "EN";
          break;
      }
      GM_setValue("lang", language);
      window.location.reload();
    });

    GM_registerMenuCommand(
      `${t("showUserCountry")} (${t(showUserCountry ? "yes" : "no")})`,
      () => {
        GM_setValue("show_user_country", !showUserCountry);
        window.location.reload();
      }
    );
  }

  //获取商店语言和区域
  const { LANGUAGE: storeLanguage, COUNTRY: userCountry } = JSON.parse(
    document
      .querySelector("#application_config")
      ?.getAttribute("data-config") ?? "{}"
  );
  const { webapi_token: accessToken } = JSON.parse(
    document
      .querySelector("#application_config")
      ?.getAttribute("data-store_user_config") ?? "{}"
  );

  const G_Objs = {};

  if (userCountry && showUserCountry) {
    const area = document.querySelector("#header_wallet_ctn");
    const span = document.createElement("span");
    span.textContent = `[${userCountry}]`;
    area.appendChild(span);
  }

  //初始化
  const pathname = window.location.pathname;
  if (pathname.startsWith("/cart")) {
    //购物车页

    function genBr() {
      return document.createElement("br");
    }
    function genBtn(text, title, onclick) {
      let btn = document.createElement("button");
      btn.textContent = text;
      btn.title = title;
      btn.className = "btn_medium btnv6_blue_hoverfade fac_cartbtns";
      btn.addEventListener("click", onclick);
      return btn;
    }
    function genSpan(text) {
      let span = document.createElement("span");
      span.textContent = text;
      return span;
    }
    function genTxt(value, placeholder) {
      const t = document.createElement("textarea");
      t.className = "fac_inputbox";
      t.placeholder = placeholder;
      t.value = value;
      return t;
    }
    function genChk(name, title, checked = false) {
      const l = document.createElement("label");
      const i = document.createElement("input");
      const s = genSpan(name);
      i.textContent = name;
      i.title = title;
      i.type = "checkbox";
      i.className = "fac_checkbox";
      i.checked = checked;
      l.title = title;
      l.appendChild(i);
      l.appendChild(s);
      return [l, i];
    }

    const savedCart = GM_getValue("fac_cart") ?? "";
    const placeHolder = [
      t("facInputBoxPlaceHolder"),
      `1. ${t("storeLink")}: https://store.steampowered.com/app/xxx`,
      `2. ${t("steamDBLink")}:  https://steamdb.info/app/xxx`,
      "3. appID:       xxx a/xxx app/xxx",
      "4. subID:       s/xxx sub/xxx",
      "5. bundleID:    b/xxx bundle/xxx",
    ].join("\n");

    const inputBox = genTxt(savedCart, placeHolder);

    function fitInputBox() {
      inputBox.style.height =
        Math.min(inputBox.value.split("\n").length * 20 + 20, 900).toString() +
        "px";
    }

    inputBox.addEventListener("input", fitInputBox);
    G_Objs.inputBox = inputBox;
    fitInputBox();

    const originResetBtn = document.querySelector("div.remove_ctn");
    if (originResetBtn != null) {
      originResetBtn.style.display = "none";
    }

    const [lblDiscount, chkDiscount] = genChk(
      t("onlyOnsale"),
      t("onlyOnsaleDesc"),
      GM_getValue("fac_discount") ?? false
    );
    G_Objs.chkDiscount = chkDiscount;

    const btnImport = genBtn(`🔼${t("import")}`, t("importDesc"), async () => {
      inputBox.value = await importCart(inputBox.value, chkDiscount.checked);
    });

    const histryPage = pathname.search("history") !== -1;
    if (histryPage) {
      btnImport.disabled = true;
      btnImport.title = t("importDesc2");
    }

    const [lblDiscount2, chkDiscount2] = genChk(
      t("onlyOnsale"),
      t("onlyOnsaleDesc2"),
      GM_getValue("fac_discount2") ?? false
    );
    G_Objs.chkDiscount2 = chkDiscount2;

    const btnExport = genBtn(`🔽${t("export")}`, t("exportDesc"), async () => {
      let currentValue = inputBox.value.trim();
      const now = new Date().toLocaleString();

      var data = await exportCart(chkDiscount2.checked);

      if (currentValue !== "") {
        ShowConfirmDialog(
          "",
          t("exportConfirm"),
          t("exportConfirmReplace"),
          t("exportConfirmAppend")
        )
          .done(() => {
            inputBox.value = `========【${now}】=========\n` + data;
            fitInputBox();
          })
          .fail((bool) => {
            if (bool) {
              inputBox.value =
                currentValue + `\n========【${now}】=========\n` + data;
              fitInputBox();
            }
          });
      } else {
        inputBox.value =
          `========【${now}】=========\n` + exportCart(chkDiscount2.checked);
        fitInputBox();
      }
    });

    const btnConvertToGift = genBtn("转送礼", "将购物车项目转换为送礼", () => {
      editCart(true, false);
    });

    const btnConvertToSelf = genBtn(
      "转自用",
      "将购物车项目转换为为自己购买",
      () => {
        editCart(false, false);
      }
    );

    const btnCopy = genBtn(`📋${t("copy")}`, t("copyDesc"), () => {
      GM_setClipboard(inputBox.value, "text");
      showAlert(t("tips"), t("copyDone"), true);
    });
    const btnClear = genBtn(`🗑️${t("reset")}`, t("resetDesc"), () => {
      ShowConfirmDialog("", t("resetConfirm"), t("ok"), t("no")).done(() => {
        inputBox.value = "";
        GM_setValue("fac_cart", "");
        fitInputBox();
      });
    });
    const btnReload = genBtn(`🔃${t("reload")}`, t("reloadDesc"), () => {
      ShowConfirmDialog("", t("reloadConfirm"), t("ok"), t("no")).done(() => {
        const s = GM_getValue("fac_cart") ?? "";
        inputBox.value = s;
        fitInputBox();
      });
    });
    const btnHistory = genBtn(`📜${t("history")}`, t("historyDesc"), () => {
      window.location.href =
        "https://help.steampowered.com/zh-cn/accountdata/ShoppingCartHistory";
    });
    const btnBack = genBtn(`↩️${t("goBack")}`, t("goBackDesc"), () => {
      window.location.href = "https://store.steampowered.com/cart/";
    });
    const btnForget = genBtn(`⚠️${t("clear")}`, t("clearDesc"), () => {
      ShowConfirmDialog("", t("clearConfirm"), t("ok"), t("no")).done(() => {
        deleteAccountCart()
          .then(() => {
            location.reload();
          })
          .catch((err) => {
            console.error(err);
            showAlert("出错", err, false);
          });
      });
    });
    const btnHelp = genBtn(`🔣${t("help")}`, t("helpDesc"), () => {
      const {
        script: { version },
      } = GM_info;
      showAlert(
        `${t("helpTitle")} ${version}`,
        [
          `<p>【🔼${t("import")}】${t("importDesc")}</p>`,
          `<p>【✅${t("onlyOnsale")}】${t("onlyOnsaleDesc")}</p>`,
          `<p>【🔽${t("export")}】${t("exportDesc")}</p>`,
          `<p>【✅${t("onlyOnsale")}】${t("onlyOnsaleDesc2")}</p>`,
          `<p>【📋${t("copy")}】${t("copyDesc")}</p>`,
          `<p>【🗑️${t("reset")}】${t("resetDesc")}。</p>`,
          `<p>【📜${t("history")}】${t("historyDesc")}</p>`,
          `<p>【↩️${t("goBack")}】${t("goBackDesc")}</p>`,
          `<p>【⚠️${t("clear")}】${t("clearDesc")}</p>`,
          `<p>【🔣${t("help")}】${t("helpDesc")}</p>`,
          `<p>【<a href="https://keylol.com/t747892-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("<br>"),
        true
      );
    });

    const btnArea = document.createElement("div");
    btnArea.appendChild(btnImport);
    // btnArea.appendChild(btnImport2);
    btnArea.appendChild(lblDiscount);
    btnArea.appendChild(genSpan(" | "));
    btnArea.appendChild(btnExport);
    btnArea.appendChild(lblDiscount2);
    btnArea.appendChild(genSpan(" | "));
    btnArea.appendChild(btnConvertToGift);
    btnArea.appendChild(btnConvertToSelf);
    btnArea.appendChild(genSpan(" | "));
    btnArea.appendChild(btnHelp);

    const btnArea2 = document.createElement("div");
    btnArea2.appendChild(btnCopy);
    btnArea2.appendChild(btnClear);
    btnArea2.appendChild(btnReload);
    btnArea2.appendChild(genSpan(" | "));
    btnArea2.appendChild(histryPage ? btnBack : btnHistory);
    btnArea2.appendChild(genSpan(" | "));
    btnArea2.appendChild(btnForget);

    const fastAddCartPanel = document.createElement("div");
    fastAddCartPanel.className = "fac_panel";

    fastAddCartPanel.appendChild(btnArea);
    fastAddCartPanel.appendChild(genBr());
    fastAddCartPanel.appendChild(inputBox);
    fastAddCartPanel.appendChild(genBr());
    fastAddCartPanel.appendChild(btnArea2);

    window.addEventListener("beforeunload", () => {
      GM_setValue("fac_cart", inputBox.value);
      GM_setValue("fac_discount", chkDiscount.checked);
      GM_setValue("fac_discount2", chkDiscount2.checked);
    });

    //等待购物车加载完毕, 显示额外面板
    const timer = setInterval(() => {
      const container = document.querySelector(
        "div[data-featuretarget='react-root']>div>div:last-child>div:last-child>div:last-child>div:nth-child(1)>div:last-child"
      );
      if (container) {
        clearInterval(timer);

        container.parentElement.insertBefore(fastAddCartPanel, container);
      }
    }, 500);
  }

  // getStoreItem([730], null, null).then((data) => console.log(data)).catch(err => console.error(err))
  // getAccountCart().then((data) => console.log(data)).catch(err => console.error(err))
  // addItemsToAccountCart(null, [28627]).then((data) => console.log(data)).catch(err => console.error(err))

  //始终在右上角显示购物车按钮
  const cart_btn = document.getElementById("store_header_cart_btn");
  if (cart_btn !== null) {
    cart_btn.style.display = "";
  }

  //导入购物车
  function importCart(text, onlyOnSale = false) {
    const regFull = new RegExp(/(app|a|bundle|b|sub|s)\/(\d+)/);
    const regShort = new RegExp(/^([\s]*|)(\d+)/);

    return new Promise(async (resolve, reject) => {
      const dialog = showAlert(
        "导入购物车",
        `<h2 id="fac_diag" class="fac_diag">${t("operation")}</h2>`,
        true
      );

      const timer = setInterval(async () => {
        let txt = document.getElementById("fac_diag");
        if (txt) {
          clearInterval(timer);

          const txts = text.split("\n");

          const result = [];

          const appIds = [];
          const subIds = [];
          const bundleIds = [];

          const targetSubIds = [];
          const targetBundleIds = [];

          try {
            txt.textContent = "0/4 开始读取输入信息";

            for (let line of txts) {
              if (line.trim() === "") {
                continue;
              }
              const tmp = line.split("#")[0];

              const match = line.match(regFull) ?? line.match(regShort);
              if (!match) {
                if (line.search("=====") === -1) {
                  result.push(`${tmp} #${t("formatError")}`);
                } else {
                  result.push(line);
                }
                continue;
              }

              let [_, type, subID] = match;
              subID = parseInt(subID);
              if (subID !== subID) {
                result.push(`${tmp} #${t("formatError")}`);
                continue;
              }

              switch (type.toLowerCase()) {
                case "":
                case "a":
                case "app":
                  type = "app";
                  appIds.push(subID);
                  break;
                case "s":
                case "sub":
                  type = "sub";
                  subIds.push(subID);
                  break;
                case "b":
                case "bundle":
                  type = "bundle";
                  bundleIds.push(subID);
                  break;
                default:
                  result.push(`${tmp} #${t("formatError")}`);
                  continue;
              }
            }

            const count = appIds.length + subIds.length + bundleIds.length;
            txt.textContent = `1/4 成功读取 ${count} 个输入内容`;

            if (count > 0) {
              txt.textContent = "1/4 开始读取游戏信息";
              const store_items = await getStoreItem(appIds, subIds, bundleIds);

              console.log(store_items);

              txt.textContent = "2/4 读取游戏信息成功";

              for (let { appid, purchase_options } of store_items) {
                if (!purchase_options) {
                  continue;
                }

                //输入值包含AppId, 解析可购买项
                if (appIds.includes(appid)) {
                  if (purchase_options.length >= 1) {
                    const {
                      packageid,
                      bundleid,
                      purchase_option_name: name,
                      discount_pct: discount,
                      formatted_final_price: price,
                    } = purchase_options[0];
                    if (discount) {
                      if (packageid) {
                        result.push(
                          `sub/${packageid} #app/${appid} #${name} 💳 ${price} 🔖 ${discount}`
                        );
                        targetSubIds.push(packageid);
                      } else if (bundleid) {
                        result.push(
                          `bundle/${bundleid} #app/${appid} #${name} 💳 ${price} 🔖 ${discount}`
                        );
                        targetBundleIds.push(bundleid);
                      }
                    } else {
                      if (packageid) {
                        if (!onlyOnSale) {
                          result.push(
                            `sub/${packageid} #app/${appid} #${name} 💳 ${price}`
                          );
                          targetSubIds.push(packageid);
                        } else {
                          result.push(
                            `sub/${packageid} #app/${appid} #排除 #${name} 💳 ${price}`
                          );
                        }
                      } else if (bundleid) {
                        if (!onlyOnSale) {
                          result.push(
                            `bundle/${bundleid} #app/${appid} #${name} 💳 ${price}`
                          );
                          targetBundleIds.push(bundleid);
                        } else {
                          result.push(
                            `bundle/${bundleid} #app/${appid} #排除 #${name} 💳 ${price}`
                          );
                        }
                      }
                    }
                  } else {
                    result.push(`${tmp} #无可购买项目`);
                  }
                }

                for (let {
                  packageid,
                  bundleid,
                  purchase_option_name: name,
                  discount_pct: discount,
                  formatted_final_price: price,
                } of purchase_options) {
                  if (discount) {
                    if (packageid && subIds.includes(packageid)) {
                      result.push(
                        `sub/${packageid} #${name} 💳 ${price} 🔖 ${discount}`
                      );
                      targetSubIds.push(packageid);
                    } else if (bundleid && bundleIds.includes(bundleid)) {
                      result.push(
                        `bundle/${bundleid} #${name} 💳 ${price} 🔖 ${discount}`
                      );
                      targetBundleIds.push(bundleid);
                    }
                  } else {
                    if (packageid && subIds.includes(packageid)) {
                      if (!onlyOnSale) {
                        result.push(`sub/${packageid} #${name} 💳 ${price}`);
                        targetSubIds.push(packageid);
                      } else {
                        result.push(
                          `sub/${packageid} #排除 #${name} 💳 ${price}`
                        );
                      }
                    } else if (bundleid && bundleIds.includes(bundleid)) {
                      if (!onlyOnSale) {
                        result.push(`bundle/${bundleid} #${name} 💳 ${price}`);
                        targetBundleIds.push(bundleid);
                      } else {
                        result.push(
                          `bundle/${bundleid} #排除 #${name} 💳 ${price}`
                        );
                      }
                    }
                  }
                }
              }

              txt.textContent = "3/4 解析游戏信息成功";

              const data = await addItemsToAccountCart(
                targetSubIds,
                targetBundleIds,
                false
              );
              console.log(data);

              txt.textContent = "4/4 导入购物车成功";

              dialog.Dismiss();

              resolve(result.join("\n"));

              setTimeout(() => {
                window.location.reload();
              }, 1000);
            } else {
              txt.textContent = "4/4 尚未输入有效内容";
              resolve(result.join("\n"));
            }
          } catch (err) {
            txt.textContent = "导出购物车失败";
            console.error(err);
            resolve(result.join("\n"));
          }
        }
      }, 200);
    });
  }
  //导出购物车
  function exportCart(onlyOnsale = false) {
    return new Promise(async (resolve, reject) => {
      const dialog = showAlert(
        "导出购物车",
        `<h2 id="fac_diag" class="fac_diag">${t("operation")}</h2>`,
        true
      );

      const timer = setInterval(async () => {
        let txt = document.getElementById("fac_diag");
        if (txt) {
          clearInterval(timer);

          const result = [];

          const subIds = [];
          const bundleIds = [];
          const gameNames = {};

          try {
            txt.textContent = "0/4 开始读取账号购物车";

            const { line_items } = await getAccountCart();

            if (line_items) {
              for (let { packageid, bundleid } of line_items) {
                if (packageid) {
                  subIds.push(packageid);
                } else if (bundleid) {
                  bundleIds.push(bundleid);
                }
              }
            }

            const count = subIds.length + bundleIds.length;
            txt.textContent = `1/4 成功读取 ${count} 个购物车内容`;

            if (count > 0) {
              txt.textContent = "1/4 开始读取游戏信息";
              const store_items = await getStoreItem(null, subIds, bundleIds);
              txt.textContent = "2/4 读取游戏信息成功";

              for (let { purchase_options } of store_items) {
                if (!purchase_options) {
                  continue;
                }

                for (let {
                  packageid,
                  bundleid,
                  purchase_option_name,
                  discount_pct,
                } of purchase_options) {
                  let key;
                  if (packageid) {
                    key = `sub/${packageid}`;
                  } else if (bundleid) {
                    key = `bundle/${bundleid}`;
                  } else {
                    continue;
                  }
                  gameNames[key] = [`${purchase_option_name}`, discount_pct];
                }
              }

              txt.textContent = "3/4 解析游戏信息成功";
              txt.textContent = "3/4 开始导出购物车信息";
              if (line_items) {
                for (let {
                  packageid,
                  bundleid,
                  price_when_added: { formatted_amount },
                } of line_items) {
                  let key;
                  if (packageid) {
                    key = `sub/${packageid}`;
                  } else if (bundleid) {
                    key = `bundle/${bundleid}`;
                  }
                  const [name, discount] = gameNames[key] ?? "_";
                  if (discount) {
                    result.push(
                      `${key} #${name} 💳 ${formatted_amount} 🔖 ${discount}`
                    );
                  } else if (!onlyOnsale) {
                    result.push(`${key} #${name} 💳 ${formatted_amount}`);
                  }
                }
              }

              txt.textContent = "3/4 导出购物车信息成功";
              dialog.Dismiss();

              resolve(result.join("\n"));
            } else {
              txt.textContent = "4/4 购物车内容为空";
              resolve(result.join("\n"));
            }
          } catch (err) {
            txt.textContent = "读取账号购物车失败";
            console.error(err);
            resolve(result.join("\n"));
          }
        }
      }, 200);
    });
  }

  //编辑购物车
  async function editCart(setToGift = false, setToPrivate = false) {
    const setGiftInfo = await inputGiftee(setToGift);

    return new Promise(async (resolve, reject) => {
      const dialog = showAlert(
        "编辑购物车",
        `<h2 id="fac_diag" class="fac_diag">${t("operation")}</h2>`,
        true
      );

      const timer = setInterval(async () => {
        let txt = document.getElementById("fac_diag");
        if (txt) {
          clearInterval(timer);

          const lineItemIds = [];

          try {
            txt.textContent = "0/3 开始读取账号购物车";

            const { line_items } = await getAccountCart();

            if (line_items) {
              for (const {
                line_item_id,
                flags: { is_gift, is_private },
                gift_info,
              } of line_items) {
                const accountid_giftee =
                  gift_info?.accountid_giftee?.toString() ?? "";

                console.log(
                  line_item_id,
                  is_gift,
                  is_private,
                  accountid_giftee
                );

                //跳过无需处理的id
                if (setToGift) {
                  if (
                    is_gift &&
                    setGiftInfo?.accountid_giftee == accountid_giftee
                  ) {
                    continue;
                  }
                } else if (setToPrivate) {
                  if (is_private) {
                    continue;
                  }
                } else {
                  if (!is_gift && !is_private) {
                    continue;
                  }
                }

                lineItemIds.push(line_item_id);
              }
            }

            console.log(lineItemIds);

            const count = lineItemIds.length;
            txt.textContent = `1/3 共计 ${count} 个待修改购物车内容`;

            if (count > 0) {
              for (let i = 0; i < count; i++) {
                const itemId = lineItemIds[i];
                await editAccountCart(
                  itemId,
                  setToGift,
                  setToPrivate,
                  setGiftInfo
                );
                txt.textContent = `2/3 当前进度 ${i + 1} / ${count}`;
              }

              txt.textContent = "3/3 批量修改购物车成功";

              setTimeout(() => dialog.Dismiss(), 1000);
              resolve();
            } else {
              txt.textContent = "3/3 购物车无需修改";

              setTimeout(() => dialog.Dismiss(), 1000);
              resolve();
            }
          } catch (err) {
            txt.textContent = "读取账号购物车失败";
            console.error(err);
            resolve();
          }
        }
      }, 200);
    });
  }

  const steamIdConvert = BigInt("0x110000100000000");

  function inputGiftee(isGift = false) {
    const nickname =
      document.querySelector("#account_pulldown")?.textContent?.trim() ??
      "unknown";

    return new Promise((resolve, reject) => {
      if (!isGift) {
        resolve(null);
      } else {
        ShowPromptDialog(
          "提示",
          "请输入礼物接收人的 SteamID 或者好友代码, 可以留空",
          "确认",
          "跳过"
        )
          .done((text) => {
            try {
              let steamId = BigInt(text.trim());
              if (steamId == 0) {
                throw "输入数值有误";
              }

              if (steamId > steamIdConvert) {
                steamId -= steamIdConvert;
              }
              const giftInfo = {
                accountid_giftee: steamId.toString(),
                gift_message: {
                  gifteename: steamId.toString(),
                  message: "Send by Fast_Add_Cart",
                  sentiment: nickname,
                  signature: nickname,
                },
                time_scheduled_send: 0,
              };
              resolve(giftInfo);
            } catch (err) {
              ShowAlertDialog("提示", "输入数值有误").then(() => resolve(null));
            }
          })
          .fail(() => {
            resolve(null);
          });
      }
    });
  }

  // 获取游戏详情
  function getStoreItem(appIds = null, subIds = null, bundleIds = null) {
    return new Promise((resolve, reject) => {
      const ids = [];
      if (appIds) {
        for (let x of appIds) {
          ids.push({ appid: x });
        }
      }
      if (subIds) {
        for (let x of subIds) {
          ids.push({ packageid: x });
        }
      }
      if (bundleIds) {
        for (let x of bundleIds) {
          ids.push({ bundleid: x });
        }
      }

      if (ids.length === 0) {
        reject([false, "未提供有效ID"]);
      }

      const payload = {
        ids,
        context: {
          language: storeLanguage,
          country_code: userCountry,
          steam_realm: "1",
        },
        data_request: {
          include_all_purchase_options: true,
        },
      };
      const json = encodeURI(JSON.stringify(payload));
      fetch(
        `https://api.steampowered.com/IStoreBrowseService/GetItems/v1/?input_json=${json}`,
        {
          method: "GET",
        }
      )
        .then(async (response) => {
          if (response.ok) {
            const {
              response: { store_items },
            } = await response.json();
            resolve(store_items);
          } else {
            reject(t("networkRequestError"));
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  //读取购物车
  function getAccountCart() {
    return new Promise((resolve, reject) => {
      fetch(
        `https://api.steampowered.com/IAccountCartService/GetCart/v1/?access_token=${accessToken}`,
        {
          method: "GET",
        }
      )
        .then(async (response) => {
          if (response.ok) {
            const {
              response: { cart },
            } = await response.json();
            resolve(cart);
          } else {
            reject(t("networkRequestError"));
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  //添加购物车
  function addItemsToAccountCart(
    subIds = null,
    bundleIds = null,
    isPrivate = false
  ) {
    return new Promise((resolve, reject) => {
      const items = [];
      if (subIds) {
        for (let x of subIds) {
          items.push({ packageid: x });
        }
      }
      if (bundleIds) {
        for (let x of bundleIds) {
          items.push({ bundleid: x });
        }
      }
      if (items.length === 0) {
        reject([false, "未提供有效ID"]);
      }

      for (let x of items) {
        x["gift_info"] = null; //giftInfo;
        x["flags"] = {
          is_gift: false,
          is_private: isPrivate == true,
        };
      }

      const payload = {
        user_country: userCountry,
        items,
        navdata: {
          domain: "store.steampowered.com",
          controller: "default",
          method: "default",
          submethod: "",
          feature: "spotlight",
          depth: 1,
          countrycode: userCountry,
          webkey: 0,
          is_client: false,
          curator_data: {
            clanid: null,
            listid: null,
          },
          is_likely_bot: false,
          is_utm: false,
        },
      };
      const json = JSON.stringify(payload);

      fetch(
        `https://api.steampowered.com/IAccountCartService/AddItemsToCart/v1/?access_token=${accessToken}`,
        {
          method: "POST",
          body: `input_json=${json}`,
          headers: {
            "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
          },
        }
      )
        .then(async (response) => {
          if (response.ok) {
            const {
              response: { cart },
            } = await response.json();
            resolve(cart);
          } else {
            reject(t("networkRequestError"));
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  //编辑购物车
  function editAccountCart(itemId, isGift, isPrivate, giftInfo = null) {
    return new Promise((resolve, reject) => {
      const payload = {
        line_item_id: itemId,
        user_country: userCountry,
        gift_info: giftInfo,
        flags: {
          is_gift: isGift,
          is_private: isPrivate,
        },
      };
      const json = JSON.stringify(payload);

      console.log(json);

      fetch(
        `https://api.steampowered.com/IAccountCartService/ModifyLineItem/v1/?access_token=${accessToken}`,
        {
          method: "POST",
          body: `input_json=${json}`,
          headers: {
            "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
          },
        }
      )
        .then(async (response) => {
          if (response.ok) {
            const {
              response: { line_item_ids, cart },
            } = await response.json();
            resolve([line_item_ids, cart]);
          } else {
            reject(t("networkRequestError"));
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  //删除购物车
  function deleteAccountCart() {
    return new Promise((resolve, reject) => {
      fetch(
        `https://api.steampowered.com/IAccountCartService/DeleteCart/v1/?access_token=${accessToken}`,
        {
          method: "POST",
        }
      )
        .then(async (response) => {
          if (response.ok) {
            const {
              response: { line_item_ids, cart },
            } = await response.json();
            resolve([line_item_ids, cart]);
          } else {
            reject(t("networkRequestError"));
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  //显示提示
  function showAlert(title, text, succ = true) {
    return ShowAlertDialog(`${succ ? "✅" : "❌"}${title}`, text);
  }
})();

GM_addStyle(`
button.fac_listbtns {
    display: none;
    position: relative;
    z-index: 100;
    padding: 1px;
  }
  a.search_result_row > button.fac_listbtns {
    top: -25px;
    left: 300px;
  }
  a.tab_item > button.fac_listbtns {
    top: -40px;
    left: 330px;
  }
  a.recommendation_link > button.fac_listbtns {
    bottom: 10px;
    right: 10px;
    position: absolute;
  }
  div.wishlist_row > button.fac_listbtns {
    top: 35%;
    right: 30%;
    position: absolute;
  }
  div.game_purchase_action > button.fac_listbtns {
    right: 8px;
    bottom: 8px;
  }
  button.fac_cartbtns {
    padding: 5px 8px;
  }
  button.fac_cartbtns:not(:last-child) {
    margin-right: 4px;
  }
  button.fac_cartbtns:not(:first-child) {
    margin-left: 4px;
  }
  a.tab_item:hover button.fac_listbtns,
  a.search_result_row:hover button.fac_listbtns,
  div.recommendation:hover button.fac_listbtns,
  div.wishlist_row:hover button.fac_listbtns {
    display: block;
  }
  div.game_purchase_action:hover > button.fac_listbtns {
    display: inline;
  }
  button.fac_choose {
    padding: 1px;
    margin: 2px 5px;
  }
  textarea.fac_inputbox {
    resize: vertical;
    font-size: 12px;
    min-height: 130px;
    width: 98%;
    background: #3d4450;
    color: #fff;
    padding: 1%;
    border: gray;
    border-radius: 5px;
  }
`);