// ==UserScript== // @name:zh-CN Steam快速添加购物车 // @name Fast_Add_Cart // @namespace https://blog.chrxw.com // @supportURL https://blog.chrxw.com/scripts.html // @contributionURL https://afdian.net/@chr233 // @version 3.0 // @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/* // @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 // @downloadURL none // ==/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": "查看购物车历史记录", "goBack": "返回", "goBackDesc": "返回你当前的购物车", "clear": "清空", "clearDesc": "清空购物车", "clearConfirm": "您确定要移除所有您购物车中的物品吗?", "clearDone": "文本框内容和保存的数据已清除", "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": "返回了未知结果", }, "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", "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 data?", "history": "History", "historyDesc": "View cart history", "goBack": "Back", "goBackDesc": "Back to your cart", "clear": "Clear", "clearDesc": "Clear cart", "clearConfirm": "Are you sure to remove all items in your cart?", "clearDone": "Textbox content and saved data has been cleared", "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", } } // 判断语言 let language = GM_getValue("lang", "ZH"); if (!language in LANG) { language = "ZH"; GM_setValue("lang", language); } // 获取翻译文本 function t(key) { return LANG[language][key] || key; } {// 自动弹出提示 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") .doen(() => { GM_setValue("lang", "EN"); 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(); }); //初始化 const pathname = window.location.pathname; if (pathname === "/search/" || pathname === "/" || pathname.startsWith("/tags/")) { //搜索页,主页,标签页 let timer = setInterval(() => { let containers = document.querySelectorAll([ "#search_resultsRows", "#tab_newreleases_content", "#tab_topsellers_content", "#tab_upcoming_content", "#tab_specials_content", "#NewReleasesRows", "#TopSellersRows", "#ConcurrentUsersRows", "#TopRatedRows", "#ComingSoonRows" ].join(",")); if (containers.length > 0) { for (let container of containers) { clearInterval(timer); for (let ele of container.children) { addButton(ele); } container.addEventListener("DOMNodeInserted", ({ relatedNode }) => { if (relatedNode.parentElement === container) { addButton(relatedNode); } }); } } }, 500); } else if (pathname.startsWith("/publisher/") || pathname.startsWith("/franchise/")) { //发行商主页 let timer = setInterval(() => { let container = document.getElementById("RecommendationsRows"); if (container != null) { clearInterval(timer); for (let ele of container.querySelectorAll("a.recommendation_link")) { addButton(ele); } container.addEventListener("DOMNodeInserted", ({ relatedNode }) => { if (relatedNode.nodeName === "DIV") { for (let ele of relatedNode.querySelectorAll("a.recommendation_link")) { addButton(ele); } } }); } }, 500); } else if (pathname.startsWith("/app/") || pathname.startsWith("/sub/") || pathname.startsWith("/bundle/")) { //商店详情页 let timer = setInterval(() => { let container = document.getElementById("game_area_purchase"); if (container != null) { clearInterval(timer); for (let ele of container.querySelectorAll("div.game_area_purchase_game")) { addButton2(ele); } } }, 500); } else if (pathname.startsWith("/wishlist/")) { //愿望单页 let timer = setInterval(() => { let container = document.getElementById("wishlist_ctn"); if (container != null) { clearInterval(timer); for (let ele of container.querySelectorAll("div.wishlist_row")) { addButton3(ele); } container.addEventListener("DOMNodeInserted", ({ relatedNode }) => { if (relatedNode.nodeName === "DIV") { for (let ele of relatedNode.querySelectorAll("div.wishlist_row")) { addButton3(ele); } } }); } }, 500); } else if (pathname.startsWith("/cart/")) { //购物车页 const continer = document.querySelector("div.cart_area_body"); 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; }; const inputBox = document.createElement("textarea"); inputBox.className = "fac_inputbox"; inputBox.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 savedCart = GM_getValue("fac_cart") ?? ""; inputBox.value = savedCart; function fitInputBox() { inputBox.style.height = Math.min(inputBox.value.split('\n').length * 20 + 20, 900).toString() + "px"; } inputBox.addEventListener("input", fitInputBox); fitInputBox(); const btnArea = document.createElement("div"); const btnImport = genBtn(`🔼${t("import")}`, t("importDesc"), async () => { inputBox.value = await importCart(inputBox.value); window.location.reload(); }); const histryPage = pathname.search("history") !== -1; if (histryPage) { btnImport.disabled = true; btnImport.title = t("importDesc2"); } const btnExport = genBtn(`🔽${t("export")}`, t("exportDesc"), () => { let currentValue = inputBox.value.trim(); if (currentValue !== "") { const now = new Date().toLocaleString(); ShowConfirmDialog("", t("exportConfirm"), t("exportConfirmReplace"), t("exportConfirmAppend")) .done(() => { inputBox.value = `========【${now}】=========\n` + exportCart(); fitInputBox(); }) .fail((bool) => { if (bool) { inputBox.value = currentValue + `\n========【${now}】=========\n` + exportCart(); fitInputBox(); } }); } else { inputBox.value = exportCart(); fitInputBox(); } }); 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(); showAlert(t("tips"), t("clearDone"), true); }); }); const btnHistory = genBtn(`📜${t("history")}`, t("historyDesc"), () => { window.location.href = "https://help.steampowered.com/zh-cn/accountdata/ShoppingCartHistory"; }); const btnBack = genBtn(`↩️${t("goBackDesc")}`, 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(() => { ForgetCart(); }); }); const btnHelp = genBtn(`🔣${t("help")}`, t("helpDesc"), () => { const { script: { version } } = GM_info; showAlert(`${t("helpTitle")} ${version}`, [ `
【🔼${t("import")}】${t("importDesc")}
`, `【🔽${t("export")}】${t("exportDesc")}
`, `【📋${t("copy")}】${t("copyDesc")}
`, `【🗑️${t("reset")}】${t("resetDesc")}。
`, `【📜${t("history")}】${t("historyDesc")}
`, `【↩️${t("goBack")}】${t("goBackDesc")}
`, `【⚠️${t("clear")}】${t("clearDesc")}
`, `【🔣${t("help")}】${t("helpDesc")}
`, `【发布帖】 【脚本反馈】 【Developed by Chr_】
` ].join("${t("addCartTips")}
`, true); let [succ, msg] = await addCart(type, subID, appID); let done = showAlert(t("operationDone"), `${msg}
`, succ); setTimeout(() => { done.Dismiss(); }, 1200); dialog.Dismiss(); if (succ) { let acBtn = btnBar.querySelector("div[class='btn_addtocart']>a"); if (acBtn) { acBtn.href = "https://store.steampowered.com/cart/"; acBtn.innerHTML = `\n\t\n${t("inCart")}\n\t\n`; } } }, false); btn.className = "fac_listbtns"; btn.textContent = "🛒"; btnBar.insertBefore(btn, firstItem); } //添加按钮 function addButton3(element) { if (element.getAttribute("added") !== null) { return; } element.setAttribute("added", ""); let appID = element.getAttribute("data-app-id"); if (appID === null) { return; } let btn = document.createElement("button"); btn.addEventListener("click", (e) => { chooseSubs(appID); e.preventDefault(); }, false); btn.className = "fac_listbtns"; btn.textContent = "🛒"; element.appendChild(btn); } //选择SUB async function chooseSubs(appID) { let dialog = showAlert(t("operation"), `${t("fetchingSubs")}
`, true); getGameSubs(appID) .then(async (subInfos) => { if (subInfos.length === 0) { showAlert(t("addCartError"), `${t("noSubFound")}, ${t("noSubDesc")}.
`, false); dialog.Dismiss(); return; } else { if (subInfos.length === 1) { let [subID, subName, discount, price] = subInfos[0]; await addCart("sub", subID, appID); let done = showAlert(t("addCartSuccess"), `${subName} - ${discount}${price}
`, true); setTimeout(() => { done.Dismiss(); }, 1200); dialog.Dismiss(); } else { let dialog2 = showAlert(t("chooseSub"), "", true); dialog.Dismiss(); await new Promise((resolve) => { let timer = setInterval(() => { if (document.getElementById("fac_choose") !== null) { clearInterval(timer); resolve(); } }, 200); }); let divContiner = document.getElementById("fac_choose"); for (let [subID, subName, discount, price] of subInfos) { let btn = document.createElement("button"); btn.addEventListener("click", async () => { let dialog = showAlert(t("operation"), `${t("add")} ${subName} - ${discount}${price} ${t("toCart")}
`, true); dialog2.Dismiss(); let [succ, msg] = await addCart("sub", subID, appID); let done = showAlert(msg, `${subName} - ${discount}${price}
`, succ); setTimeout(() => { done.Dismiss(); }, 1200); dialog.Dismiss(); }); btn.textContent = `🛒${t("addCart")}`; btn.className = "fac_choose"; let p = document.createElement("p"); p.textContent = `${subName} - ${discount}${price}`; p.appendChild(btn); divContiner.appendChild(p); } } } }) .catch((err) => { let done = showAlert(t("networkError"), `${err}
`, false); setTimeout(() => { done.Dismiss(); }, 2000); dialog.Dismiss(); }); } //读取sub信息 function getGameSubs(appID) { return new Promise((resolve, reject) => { const regPure = new RegExp(/ - [^-]*$/, ""); const regSymbol = new RegExp(/[>-] ([^>-]+) [\d.]+$/, ""); const lang = document.cookie.replace(/(?:(?:^|.*;\s*)Steam_Language\s*\=\s*([^;]*).*$)|^.*$/, "$1"); fetch(`https://store.steampowered.com/api/appdetails?appids=${appID}&lang=${lang}`, { method: "GET", credentials: "include", }) .then(async (response) => { if (response.ok) { let data = await response.json(); let result = data[appID]; if (result.success !== true) { reject(t("unrecognizedResult")); } let subInfos = []; for (let pkg of result.data.package_groups) { for (let sub of pkg.subs) { const { packageid, option_text, percent_savings_text, price_in_cents_with_discount } = sub; if (price_in_cents_with_discount > 0) { //排除免费SUB const symbol = option_text.match(regSymbol)?.pop(); const subName = option_text.replace(regPure, ""); const price = "💳" + price_in_cents_with_discount / 100 + " " + symbol; const discount = percent_savings_text !== " " ? "🔖" + percent_savings_text + " " : ""; subInfos.push([packageid, subName, discount, price]); } } } console.info(subInfos); resolve(subInfos); } else { reject(t("networkRequestError")); } }).catch((err) => { reject(err); }); }); } //添加购物车,只支持subID和bundleID function addCart(type = "sub", subID, appID = null) { window.localStorage["fac_subid"] = subID; return new Promise((resolve, reject) => { let data = { action: "add_to_cart", originating_snr: "1_store-navigation__", sessionid: document.cookie.replace(/(?:(?:^|.*;\s*)sessionid\s*\=\s*([^;]*).*$)|^.*$/, "$1"), snr: "1_5_9__403", } data[`${type}id`] = String(subID); let s = []; for (let k in data) { s += `${k}=${encodeURIComponent(data[k])}&`; } fetch("https://store.steampowered.com/cart/", { method: "POST", credentials: "include", body: s, headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }, }) .then(async (response) => { if (response.ok) { let data = await response.text(); if (appID !== null) { const regIfSucc = new RegExp("app\/" + appID); if (data.search(regIfSucc) !== -1) { resolve([true, t("addCartSuccess")]); } else { resolve([false, t("addCartError")]); } } else { resolve([true, t("addCartSuccess")]); } } else { resolve([false, t("networkRequestError")]); } }).catch((err) => { console.error(err); resolve([false, `${t("unknownError")}: ${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 10px; } button.fac_cartbtns:not(:last-child) { margin-right: 5px; } button.fac_cartbtns:not(:first-child) { margin-left: 5px; } 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 { height: 130px; resize: vertical; font-size: 10px; min-height: 130px; } textarea.fac_diag { height: 150px; width: 600px; resize: vertical; font-size: 10px; margin-bottom: 5px; 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; } `);