// ==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 4.1 // @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: "查看购物车历史记录", 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: "尚未打折, 跳过", }, 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", }, }; // 判断语言 let language = GM_getValue("lang", "ZH"); if (!language in LANG) { language = "ZH"; GM_setValue("lang", language); } // 获取翻译文本 const t = (key) => 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" ) .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(); }); } //获取商店语言和区域 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 = {}; //初始化 const pathname = window.location.pathname; if ( pathname === "/search/" || pathname === "/" || pathname.startsWith("/tags/") ) { //搜索页,主页,标签页 return; // 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); // } // }); // } // const searchBar = document.querySelector(".searchbar>.searchbar_left"); // if (searchBar !== null) { // let btn = document.createElement("button"); // btn.addEventListener( // "click", // (e) => { // e.preventDefault(); // const savedCart = // GM_getValue("btnv6_blue_hoverfade btn_small") ?? ""; // const cartItems = savedCart.split("\n"); // const regFull = new RegExp(/((app|a|bundle|b|sub|s)\/(\d+))/); // const regShort = new RegExp(/^(([\s]*|)(\d+))/); // const dataMap = new Set(); // for (let line of cartItems) { // let match = line.match(regFull) ?? line.match(regShort); // if (match) { // let [_, link, _1, _2] = match; // dataMap.add(link); // } // } // const now = new Date().toLocaleString(); // cartItems.push(`========【${now}】=========`); // const rows = document.querySelectorAll("#search_resultsRows>a"); // for (let row of rows) { // if ( // row.className.includes("ds_owned") || // row.className.includes("ds_ignored") // ) { // continue; // } // const url = row.href; // const title = // row.querySelector("span.title")?.textContent ?? "null"; // let match = url.match(regFull); // if (match) { // let [_, link, _1, _2] = match; // if (!dataMap.has(link)) { // cartItems.push(`${link} #${title}`); // } // } // } // GM_setValue("fac_cart", cartItems.join("\n")); // const dialog = showAlert( // t("batchExtractDone"), // t("batchDesc"), // true // ); // setTimeout(() => { // dialog.Dismiss(); // }, 1500); // }, // false // ); // btn.className = "btnv6_blue_hoverfade btn_small"; // btn.innerHTML = `${t("batchExtract")}`; // searchBar.appendChild(btn); // } // } // }, 500); } else if ( pathname.startsWith("/publisher/") || pathname.startsWith("/franchise/") || pathname.startsWith("/developer/") ) { //发行商主页 return; // 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/") ) { //商店详情页 return; // 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/")) { //愿望单页 return; // 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")) { //购物车页 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"); // btnImport2.disabled = true; // btnImport2.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 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}`, [ `
【🔼${t("import")}】${t("importDesc")}
`, `【✅${t("onlyOnsale")}】${t("onlyOnsaleDesc")}
`, `【🔽${t("export")}】${t("exportDesc")}
`, `【✅${t("onlyOnsale")}】${t("onlyOnsaleDesc2")}
`, `【📋${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("