Steam快速添加购物车

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

当前为 2022-05-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name:zh-CN Steam快速添加购物车
  3. // @name Fast_Add_Cart
  4. // @namespace https://blog.chrxw.com
  5. // @supportURL https://blog.chrxw.com/scripts.html
  6. // @contributionURL https://afdian.net/@chr233
  7. // @version 3.2
  8. // @description:zh-CN 超级方便的添加购物车体验, 不用跳转商店页, 附带导入导出购物车功能.
  9. // @description Add to cart without redirect to cart page, also provide import/export cart feature.
  10. // @author Chr_
  11. // @match https://store.steampowered.com/*
  12. // @license AGPL-3.0
  13. // @icon https://blog.chrxw.com/favicon.ico
  14. // @grant GM_addStyle
  15. // @grant GM_setClipboard
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // @grant GM_registerMenuCommand
  19. // ==/UserScript==
  20.  
  21. (async () => {
  22. "use strict";
  23.  
  24. // 多语言
  25. const LANG = {
  26. "ZH": {
  27. "langName": "中文",
  28. "changeLang": "修改插件语言",
  29. "facInputBoxPlaceHolder": "一行一条, 自动忽略【#】后面的内容, 支持的格式如下: (自动保存)",
  30. "storeLink": "商店链接",
  31. "steamDBLink": "DB链接",
  32. "import": "批量导入",
  33. "importDesc": "从文本框批量添加购物车",
  34. "importDesc2": "当前页面无法导入购物车",
  35. "export": "导出",
  36. "exportDesc": "将购物车内容导出至文本框",
  37. "exportConfirm": "输入框中含有内容, 请选择操作?",
  38. "exportConfirmReplace": "覆盖原有内容",
  39. "exportConfirmAppend": "添加到最后",
  40. "copy": "复制",
  41. "copyDesc": "复制文本框中的内容",
  42. "copyDone": "复制到剪贴板成功",
  43. "reset": "清除",
  44. "resetDesc": "清除文本框和已保存的数据",
  45. "resetConfirm": "您确定要清除文本框和已保存的数据吗?",
  46. "history": "历史",
  47. "historyDesc": "查看购物车历史记录",
  48. "goBack": "返回",
  49. "goBackDesc": "返回你当前的购物车",
  50. "clear": "清空",
  51. "clearDesc": "清空购物车",
  52. "clearConfirm": "您确定要移除所有您购物车中的物品吗?",
  53. "clearDone": "文本框内容和保存的数据已清除",
  54. "help": "帮助",
  55. "helpDesc": "显示帮助",
  56. "helpTitle": "插件版本",
  57. "formatError": "格式有误",
  58. "chooseSub": "请选择SUB",
  59. "operation": "操作中……",
  60. "operationDone": "操作完成",
  61. "addCart": "添加购物车",
  62. "addCartTips": "添加到购物车……",
  63. "addCartErrorSubNotFount": "未识别到SubID",
  64. "noSubDesc": "可能尚未发行或者是免费游戏",
  65. "inCart": "在购物车中",
  66. "importingTitle": "正在导入购物车……",
  67. "add": "添加",
  68. "toCart": "到购物车",
  69. "tips": "提示",
  70. "ok": "是",
  71. "no": "否",
  72. "fetchingSubs": "读取可用SUB",
  73. "noSubFound": "未找到可用SUB",
  74. "networkError": "网络错误",
  75. "addCartSuccess": "添加购物车成功",
  76. "addCartError": "添加购物车失败",
  77. "networkRequestError": "网络请求失败",
  78. "unknownError": "未知错误",
  79. "unrecognizedResult": "返回了未知结果",
  80. "batchExtract": "批量提取",
  81. "batchExtractDone": "批量提取完成",
  82. "batchDesc": "AppID已提取, 可以在购物车页批量导入",
  83. },
  84. "EN": {
  85. "langName": "English",
  86. "changeLang": "Change plugin language",
  87. "facInputBoxPlaceHolder": "One line one item, ignore the content after #, support format: (auto save)",
  88. "storeLink": "Store link",
  89. "steamDBLink": "DB link",
  90. "import": "Import",
  91. "importDesc": "Batch add cart from textbox",
  92. "importDesc2": "Current page can't import cart",
  93. "export": "Export",
  94. "exportDesc": "Export cart content to textbox",
  95. "exportConfirm": "Textbox contains content, please choose operation?",
  96. "exportConfirmReplace": "Replace original content",
  97. "exportConfirmAppend": "Append to the end",
  98. "copy": "Copy",
  99. "copyDesc": "Copy textbox content",
  100. "copyDone": "Copy to clipboard success",
  101. "reset": "Reset",
  102. "resetDesc": "Clear textbox and saved data",
  103. "resetConfirm": "Are you sure to clear textbox and saved data?",
  104. "history": "History",
  105. "historyDesc": "View cart history",
  106. "goBack": "Back",
  107. "goBackDesc": "Back to your cart",
  108. "clear": "Clear",
  109. "clearDesc": "Clear cart",
  110. "clearConfirm": "Are you sure to remove all items in your cart?",
  111. "clearDone": "Textbox content and saved data has been cleared",
  112. "help": "Help",
  113. "helpDesc": "Show help",
  114. "helpTitle": "Plugin Version",
  115. "formatError": "Format error",
  116. "chooseSub": "Please choose SUB",
  117. "operation": "Operation in progress……",
  118. "operationDone": "Operation done",
  119. "addCart": "Add cart",
  120. "addCartTips": "Adding to cart……",
  121. "addCartErrorSubNotFount": "Unrecognized SubID",
  122. "noSubDesc": "Maybe not released or free game",
  123. "inCart": "In cart",
  124. "importingTitle": "Importing cart……",
  125. "add": "Add",
  126. "toCart": "To cart",
  127. "tips": "Tips",
  128. "ok": "OK",
  129. "no": "No",
  130. "fetchingSubs": "Fetching available SUB",
  131. "noSubFound": "No available SUB",
  132. "networkError": "Network error",
  133. "addCartSuccess": "Add cart success",
  134. "addCartError": "Add cart failed",
  135. "networkRequestError": "Network request failed",
  136. "unknownError": "Unknown error",
  137. "unrecognizedResult": "Returned unrecognized result",
  138. "batchExtract": "Extract Items",
  139. "batchExtractDone": "Batch Extract Done",
  140. "batchDesc": "AppID list now saved, goto cart page to use batch import.",
  141. }
  142. }
  143.  
  144. // 判断语言
  145. let language = GM_getValue("lang", "ZH");
  146. if (!language in LANG) {
  147. language = "ZH";
  148. GM_setValue("lang", language);
  149. }
  150. // 获取翻译文本
  151. function t(key) {
  152. return LANG[language][key] || key;
  153. }
  154. {// 自动弹出提示
  155. const languageTips = GM_getValue("languageTips", true);
  156. if (languageTips && language === "ZH") {
  157. if (!document.querySelector("html").lang.startsWith("zh")) {
  158. ShowConfirmDialog("tips", "Fast add cart now support English, switch?", "Using English", "Don't show again")
  159. .done(() => {
  160. GM_setValue("lang", "EN");
  161. GM_setValue("languageTips", false);
  162. window.location.reload();
  163. })
  164. .fail((bool) => {
  165. if (bool) {
  166. showAlert("", "You can switch the plugin's language using TamperMonkey's menu.");
  167. GM_setValue("languageTips", false);
  168. }
  169. });
  170. }
  171. }
  172. }
  173. GM_registerMenuCommand(`${t("changeLang")} (${t("langName")})`, () => {
  174. switch (language) {
  175. case "EN":
  176. language = "ZH";
  177. break;
  178. case "ZH":
  179. language = "EN";
  180. break;
  181. }
  182. GM_setValue("lang", language);
  183. window.location.reload();
  184. });
  185. //初始化
  186. const pathname = window.location.pathname;
  187. if (pathname === "/search/" || pathname === "/" || pathname.startsWith("/tags/")) { //搜索页,主页,标签页
  188. let timer = setInterval(() => {
  189. let containers = document.querySelectorAll([
  190. "#search_resultsRows",
  191. "#tab_newreleases_content",
  192. "#tab_topsellers_content",
  193. "#tab_upcoming_content",
  194. "#tab_specials_content",
  195. "#NewReleasesRows",
  196. "#TopSellersRows",
  197. "#ConcurrentUsersRows",
  198. "#TopRatedRows",
  199. "#ComingSoonRows"
  200. ].join(","));
  201. if (containers.length > 0) {
  202. for (let container of containers) {
  203. clearInterval(timer);
  204. for (let ele of container.children) {
  205. addButton(ele);
  206. }
  207. container.addEventListener("DOMNodeInserted", ({ relatedNode }) => {
  208. if (relatedNode.parentElement === container) {
  209. addButton(relatedNode);
  210. }
  211. });
  212. }
  213.  
  214. const searchBar = document.querySelector(".searchbar>.searchbar_left");
  215. if (searchBar !== null) {
  216. let btn = document.createElement("button");
  217. btn.addEventListener("click", (e) => {
  218. e.preventDefault();
  219. const savedCart = GM_getValue("btnv6_blue_hoverfade btn_small") ?? "";
  220. const cartItems = savedCart.split("\n");
  221. const regFull = new RegExp(/((app|a|bundle|b|sub|s)\/(\d+))/);
  222. const regShort = new RegExp(/^(([\s]*|)(\d+))/);
  223. const dataMap = new Set();
  224.  
  225. for (let line of cartItems) {
  226. let match = line.match(regFull) ?? line.match(regShort);
  227. if (match) {
  228. let [_, link, _1, _2] = match;
  229. dataMap.add(link);
  230. }
  231. }
  232.  
  233. const now = new Date().toLocaleString();
  234. cartItems.push(`========【${now}】=========`);
  235.  
  236. const rows = document.querySelectorAll("#search_resultsRows>a");
  237. for (let row of rows) {
  238. const url = row.href;
  239. const title = row.querySelector("span.title")?.textContent ?? "null";
  240.  
  241. let match = url.match(regFull);
  242. if (match) {
  243. let [_, link, _1, _2] = match;
  244.  
  245. if (!dataMap.has(link)) {
  246. cartItems.push(`${link} #${title}`);
  247. }
  248. }
  249. }
  250. GM_setValue("fac_cart", cartItems.join("\n"));
  251. const dialog = showAlert(t("batchExtractDone"), t("batchDesc"), true);
  252. setTimeout(() => { dialog.Dismiss(); }, 1500);
  253. }, false);
  254. btn.className = "btnv6_blue_hoverfade btn_small";
  255. btn.innerHTML = `<span>${t("batchExtract")}</span>`;
  256. searchBar.appendChild(btn);
  257. }
  258. }
  259. }, 500);
  260. } else if (pathname.startsWith("/publisher/") || pathname.startsWith("/franchise/")) { //发行商主页
  261. let timer = setInterval(() => {
  262. let container = document.getElementById("RecommendationsRows");
  263. if (container != null) {
  264. clearInterval(timer);
  265. for (let ele of container.querySelectorAll("a.recommendation_link")) {
  266. addButton(ele);
  267. }
  268. container.addEventListener("DOMNodeInserted", ({ relatedNode }) => {
  269. if (relatedNode.nodeName === "DIV") {
  270. for (let ele of relatedNode.querySelectorAll("a.recommendation_link")) {
  271. addButton(ele);
  272. }
  273. }
  274. });
  275. }
  276. }, 500);
  277. } else if (pathname.startsWith("/app/") || pathname.startsWith("/sub/") || pathname.startsWith("/bundle/")) { //商店详情页
  278. let timer = setInterval(() => {
  279. let container = document.getElementById("game_area_purchase");
  280. if (container != null) {
  281. clearInterval(timer);
  282. for (let ele of container.querySelectorAll("div.game_area_purchase_game")) {
  283. addButton2(ele);
  284. }
  285. }
  286. }, 500);
  287. } else if (pathname.startsWith("/wishlist/")) { //愿望单页
  288. let timer = setInterval(() => {
  289. let container = document.getElementById("wishlist_ctn");
  290. if (container != null) {
  291. clearInterval(timer);
  292.  
  293. for (let ele of container.querySelectorAll("div.wishlist_row")) {
  294. addButton3(ele);
  295. }
  296. container.addEventListener("DOMNodeInserted", ({ relatedNode }) => {
  297. if (relatedNode.nodeName === "DIV") {
  298. for (let ele of relatedNode.querySelectorAll("div.wishlist_row")) {
  299. addButton3(ele);
  300. }
  301. }
  302. });
  303. }
  304. }, 500);
  305. } else if (pathname.startsWith("/cart/")) { //购物车页
  306. const continer = document.querySelector("div.cart_area_body");
  307.  
  308. function genBr() { return document.createElement("br"); };
  309. function genBtn(text, title, onclick) {
  310. let btn = document.createElement("button");
  311. btn.textContent = text;
  312. btn.title = title;
  313. btn.className = "btn_medium btnv6_blue_hoverfade fac_cartbtns";
  314. btn.addEventListener("click", onclick);
  315. return btn;
  316. };
  317. function genSpan(text) {
  318. let span = document.createElement("span");
  319. span.textContent = text;
  320. return span;
  321. };
  322. const inputBox = document.createElement("textarea");
  323. inputBox.className = "fac_inputbox";
  324. inputBox.placeholder = [t("facInputBoxPlaceHolder"),
  325. `1. ${t("storeLink")}: https://store.steampowered.com/app/xxx`,
  326. `2. ${t("steamDBLink")}: https://steamdb.info/app/xxx`,
  327. "3. appID: xxx a/xxx app/xxx",
  328. "4. subID: s/xxx sub/xxx",
  329. "5. bundleID: b/xxx bundle/xxx"
  330. ].join("\n");
  331. const savedCart = GM_getValue("fac_cart") ?? "";
  332. inputBox.value = savedCart;
  333.  
  334. function fitInputBox() {
  335. inputBox.style.height = Math.min(inputBox.value.split('\n').length * 20 + 20, 900).toString() + "px";
  336. }
  337.  
  338. inputBox.addEventListener("input", fitInputBox);
  339.  
  340. fitInputBox();
  341.  
  342. const btnArea = document.createElement("div");
  343. const btnImport = genBtn(`🔼${t("import")}`, t("importDesc"), async () => {
  344. inputBox.value = await importCart(inputBox.value);
  345. window.location.reload();
  346. });
  347. const histryPage = pathname.search("history") !== -1;
  348. if (histryPage) {
  349. btnImport.disabled = true;
  350. btnImport.title = t("importDesc2");
  351. }
  352.  
  353. const btnExport = genBtn(`🔽${t("export")}`, t("exportDesc"), () => {
  354. let currentValue = inputBox.value.trim();
  355. if (currentValue !== "") {
  356. const now = new Date().toLocaleString();
  357. ShowConfirmDialog("", t("exportConfirm"), t("exportConfirmReplace"), t("exportConfirmAppend"))
  358. .done(() => {
  359. inputBox.value = `========【${now}】=========\n` + exportCart();
  360. fitInputBox();
  361. })
  362. .fail((bool) => {
  363. if (bool) {
  364. inputBox.value = currentValue + `\n========【${now}】=========\n` + exportCart();
  365. fitInputBox();
  366. }
  367. });
  368. } else {
  369. inputBox.value = exportCart();
  370. fitInputBox();
  371. }
  372. });
  373. const btnCopy = genBtn(`📋${t("copy")}`, t("copyDesc"), () => {
  374. GM_setClipboard(inputBox.value, "text");
  375. showAlert(t("tips"), t("copyDone"), true);
  376. });
  377. const btnClear = genBtn(`🗑️${t("reset")}`, t("resetDesc"), () => {
  378. ShowConfirmDialog("", t("resetConfirm"), t("ok"), t("no"))
  379. .done(() => {
  380. inputBox.value = "";
  381. GM_setValue("fac_cart", "");
  382. fitInputBox();
  383. showAlert(t("tips"), t("clearDone"), true);
  384. });
  385. });
  386. const btnHistory = genBtn(`📜${t("history")}`, t("historyDesc"), () => {
  387. window.location.href = "https://help.steampowered.com/zh-cn/accountdata/ShoppingCartHistory";
  388. });
  389. const btnBack = genBtn(`↩️${t("goBack")}`, t("goBackDesc"), () => {
  390. window.location.href = "https://store.steampowered.com/cart/";
  391. });
  392. const btnForget = genBtn(`⚠️${t("clear")}`, t("clearDesc"), () => {
  393. ShowConfirmDialog("", t("clearConfirm"), t("ok"), t("no"))
  394. .done(() => {
  395. ForgetCart();
  396. });
  397. });
  398. const btnHelp = genBtn(`🔣${t("help")}`, t("helpDesc"), () => {
  399. const { script: { version } } = GM_info;
  400. showAlert(`${t("helpTitle")} ${version}`, [
  401. `<p>【🔼${t("import")}】${t("importDesc")}</p>`,
  402. `<p>【🔽${t("export")}】${t("exportDesc")}</p>`,
  403. `<p>【📋${t("copy")}】${t("copyDesc")}</p>`,
  404. `<p>【🗑️${t("reset")}】${t("resetDesc")}。</p>`,
  405. `<p>【📜${t("history")}】${t("historyDesc")}</p>`,
  406. `<p>【↩️${t("goBack")}】${t("goBackDesc")}</p>`,
  407. `<p>【⚠️${t("clear")}】${t("clearDesc")}</p>`,
  408. `<p>【🔣${t("help")}】${t("helpDesc")}</p>`,
  409. `<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>`
  410. ].join("<br>"), true);
  411. });
  412.  
  413. btnArea.appendChild(btnImport);
  414. btnArea.appendChild(btnExport);
  415. btnArea.appendChild(genSpan(" | "));
  416. btnArea.appendChild(btnCopy);
  417. btnArea.appendChild(btnClear);
  418. btnArea.appendChild(genSpan(" | "));
  419. btnArea.appendChild(histryPage ? btnBack : btnHistory);
  420. btnArea.appendChild(genSpan(" | "));
  421. btnArea.appendChild(btnForget);
  422. btnArea.appendChild(genSpan(" | "));
  423. btnArea.appendChild(btnHelp);
  424.  
  425. continer.appendChild(btnArea);
  426. btnArea.appendChild(genBr());
  427. btnArea.appendChild(genBr());
  428. continer.appendChild(inputBox);
  429.  
  430. window.addEventListener("beforeunload", () => { GM_setValue("fac_cart", inputBox.value); })
  431. }
  432.  
  433. //始终在右上角显示购物车按钮
  434. const cart_btn = document.getElementById("store_header_cart_btn");
  435. if (cart_btn !== null) { cart_btn.style.display = ""; }
  436.  
  437. //导入购物车
  438. function importCart(text) {
  439. return new Promise(async (resolve, reject) => {
  440. const regFull = new RegExp(/(app|a|bundle|b|sub|s)\/(\d+)/);
  441. const regShort = new RegExp(/^([\s]*|)(\d+)/);
  442. let lines = [];
  443.  
  444. let dialog = showAlert(t("importingTitle"), `<textarea id="fac_diag" class="fac_diag">${t("operation")}</textarea>`, true);
  445.  
  446. let timer = setInterval(async () => {
  447. let txt = document.getElementById("fac_diag");
  448. if (txt !== null) {
  449. clearInterval(timer);
  450. for (let line of text.split("\n").reverse()) {
  451. if (line.trim() === "") {
  452. continue;
  453. }
  454. let match = line.match(regFull) ?? line.match(regShort);
  455. if (!match) {
  456. if (line.search("=====") === -1) {
  457. let tmp = line.split("#")[0];
  458. lines.push(`${tmp} #${t("formatError")}`);
  459. } else {
  460. lines.push(line);
  461. }
  462. continue;
  463. }
  464. let [_, type, subID] = match;
  465. switch (type.toLowerCase()) {
  466. case "":
  467. case "a":
  468. case "app":
  469. type = "app";
  470. break;
  471. case "s":
  472. case "sub":
  473. type = "sub";
  474. break;
  475. case "b":
  476. case "bundle":
  477. type = "bundle";
  478. break;
  479. default:
  480. let tmp = line.split("#")[0];
  481. lines.push(`${tmp} #${t("formatError")}`);
  482. continue;
  483. }
  484.  
  485. if (type === "sub" || type === "bundle") {
  486. let [succ, msg] = await addCart(type, subID, "");
  487. lines.push(`${type}/${subID} #${msg}`);
  488. } else {
  489. try {
  490. let subInfos = await getGameSubs(subID);
  491. let [sID, subName, discount, price] = subInfos[0];
  492. let [succ, msg] = await addCart("sub", sID, subID);
  493. lines.push(`${type}/${subID} #${subName} - ${discount}${price} ${msg}`);
  494. } catch (e) {
  495. lines.push(`${type}/${subID} #${t("noSubFound")}`);
  496. }
  497. }
  498. txt.value = lines.join("\n");
  499. txt.scrollTop = txt.scrollHeight;
  500. }
  501. }
  502.  
  503. dialog.Dismiss();
  504. resolve(lines.join("\n"));
  505. }, 200);
  506. });
  507. }
  508. //导出购物车
  509. function exportCart() {
  510. const regMatch = new RegExp(/(app|sub|bundle)_(\d+)/);
  511. let data = [];
  512. for (let item of document.querySelectorAll("div.cart_item_list>div.cart_row ")) {
  513. let itemKey = item.getAttribute("data-ds-itemkey");
  514. let name = item.querySelector(".cart_item_desc>a").innerText.trim();
  515. let match = itemKey.toLowerCase().match(regMatch);
  516. if (match) {
  517. let [_, type, id] = match;
  518. data.push(`${type}/${id} #${name}`);
  519. }
  520. }
  521. return data.join("\n");
  522. }
  523. //添加按钮
  524. function addButton(element) {
  525. if (element.getAttribute("added") !== null) { return; }
  526. element.setAttribute("added", "");
  527.  
  528. if (element.href === undefined) { return; }
  529.  
  530. let appID = (element.href.match(/\/app\/(\d+)/) ?? [null, null])[1];
  531. if (appID === null) { return; }
  532.  
  533. let btn = document.createElement("button");
  534. btn.addEventListener("click", (e) => {
  535. chooseSubs(appID);
  536. e.preventDefault();
  537. }, false);
  538. btn.className = "fac_listbtns";
  539. btn.textContent = "🛒";
  540. element.appendChild(btn);
  541. }
  542. //添加按钮
  543. function addButton2(element) {
  544. if (element.getAttribute("added") !== null) { return; }
  545. element.setAttribute("added", "");
  546. let type, subID;
  547.  
  548. let parentElement = element.parentElement;
  549.  
  550. if (parentElement.hasAttribute("data-ds-itemkey")) {
  551. let itemKey = parentElement.getAttribute("data-ds-itemkey");
  552. let match = itemKey.toLowerCase().match(/(app|sub|bundle)_(\d+)/);
  553. if (match) { [, type, subID] = match; }
  554. } else if (parentElement.hasAttribute("data-ds-bundleid") || parentElement.hasAttribute("data-ds-subid")) {
  555. subID = parentElement.getAttribute("data-ds-subid") ?? parentElement.getAttribute("data-ds-bundleid");
  556. type = parentElement.hasAttribute("data-ds-subid") ? "sub" : "bundle";
  557. } else {
  558. let match = element.id.match(/cart_(\d+)/);
  559. if (match) {
  560. type = "sub";
  561. [, subID] = match;
  562. }
  563. }
  564.  
  565. if (type === undefined || subID === undefined) {
  566. console.warn(t("addCartErrorSubNotFount"));
  567. return;
  568. }
  569.  
  570. const btnBar = element.querySelector("div.game_purchase_action");
  571. const firstItem = element.querySelector("div.game_purchase_action_bg");
  572. if (btnBar === null || firstItem == null || type === undefined || subID === undefined) { return; }
  573. let appID = (window.location.pathname.match(/\/(app)\/(\d+)/) ?? [null, null, null])[2];
  574. let btn = document.createElement("button");
  575. btn.addEventListener("click", async () => {
  576. let dialog = showAlert(t("operation"), `<p>${t("addCartTips")}</p>`, true);
  577. let [succ, msg] = await addCart(type, subID, appID);
  578. let done = showAlert(t("operationDone"), `<p>${msg}</p>`, succ);
  579. setTimeout(() => { done.Dismiss(); }, 1200);
  580. dialog.Dismiss();
  581. if (succ) {
  582. let acBtn = btnBar.querySelector("div[class='btn_addtocart']>a");
  583. if (acBtn) {
  584. acBtn.href = "https://store.steampowered.com/cart/";
  585. acBtn.innerHTML = `\n\t\n<span>${t("inCart")}</span>\n\t\n`;
  586. }
  587. }
  588. }, false);
  589. btn.className = "fac_listbtns";
  590. btn.textContent = "🛒";
  591. btnBar.insertBefore(btn, firstItem);
  592. }
  593. //添加按钮
  594. function addButton3(element) {
  595. if (element.getAttribute("added") !== null) { return; }
  596. element.setAttribute("added", "");
  597.  
  598. let appID = element.getAttribute("data-app-id");
  599. if (appID === null) { return; }
  600.  
  601. let btn = document.createElement("button");
  602. btn.addEventListener("click", (e) => {
  603. chooseSubs(appID);
  604. e.preventDefault();
  605. }, false);
  606. btn.className = "fac_listbtns";
  607. btn.textContent = "🛒";
  608. element.appendChild(btn);
  609. }
  610. //选择SUB
  611. async function chooseSubs(appID) {
  612. let dialog = showAlert(t("operation"), `<p>${t("fetchingSubs")}</p>`, true);
  613. getGameSubs(appID)
  614. .then(async (subInfos) => {
  615. if (subInfos.length === 0) {
  616. showAlert(t("addCartError"), `<p>${t("noSubFound")}, ${t("noSubDesc")}.</p>`, false);
  617. dialog.Dismiss();
  618. return;
  619. } else {
  620. if (subInfos.length === 1) {
  621. let [subID, subName, discount, price] = subInfos[0];
  622. await addCart("sub", subID, appID);
  623. let done = showAlert(t("addCartSuccess"), `<p>${subName} - ${discount}${price}</p>`, true);
  624. setTimeout(() => { done.Dismiss(); }, 1200);
  625. dialog.Dismiss();
  626. } else {
  627. let dialog2 = showAlert(t("chooseSub"), "<div id=fac_choose></div>", true);
  628. dialog.Dismiss();
  629. await new Promise((resolve) => {
  630. let timer = setInterval(() => {
  631. if (document.getElementById("fac_choose") !== null) {
  632. clearInterval(timer);
  633. resolve();
  634. }
  635. }, 200);
  636. });
  637. let divContiner = document.getElementById("fac_choose");
  638. for (let [subID, subName, discount, price] of subInfos) {
  639. let btn = document.createElement("button");
  640. btn.addEventListener("click", async () => {
  641. let dialog = showAlert(t("operation"), `<p>${t("add")} ${subName} - ${discount}${price} ${t("toCart")}</p>`, true);
  642. dialog2.Dismiss();
  643. let [succ, msg] = await addCart("sub", subID, appID);
  644. let done = showAlert(msg, `<p>${subName} - ${discount}${price}</p>`, succ);
  645. setTimeout(() => { done.Dismiss(); }, 1200);
  646. dialog.Dismiss();
  647. });
  648. btn.textContent = `🛒${t("addCart")}`;
  649. btn.className = "fac_choose";
  650. let p = document.createElement("p");
  651. p.textContent = `${subName} - ${discount}${price}`;
  652. p.appendChild(btn);
  653. divContiner.appendChild(p);
  654. }
  655. }
  656. }
  657. })
  658. .catch((err) => {
  659. let done = showAlert(t("networkError"), `<p>${err}</p>`, false);
  660. setTimeout(() => { done.Dismiss(); }, 2000);
  661. dialog.Dismiss();
  662. });
  663. }
  664. //读取sub信息
  665. function getGameSubs(appID) {
  666. return new Promise((resolve, reject) => {
  667. const regPure = new RegExp(/ - [^-]*$/, "");
  668. const regSymbol = new RegExp(/[>-] ([^>-]+) [\d.]+$/, "");
  669. const lang = document.cookie.replace(/(?:(?:^|.*;\s*)Steam_Language\s*\=\s*([^;]*).*$)|^.*$/, "$1");
  670. fetch(`https://store.steampowered.com/api/appdetails?appids=${appID}&lang=${lang}`, {
  671. method: "GET",
  672. credentials: "include",
  673. })
  674. .then(async (response) => {
  675. if (response.ok) {
  676. let data = await response.json();
  677. let result = data[appID];
  678. if (result.success !== true) {
  679. reject(t("unrecognizedResult"));
  680. }
  681. let subInfos = [];
  682. for (let pkg of result.data.package_groups) {
  683. for (let sub of pkg.subs) {
  684. const { packageid, option_text, percent_savings_text, price_in_cents_with_discount } = sub;
  685. if (price_in_cents_with_discount > 0) { //排除免费SUB
  686. const symbol = option_text.match(regSymbol)?.pop();
  687. const subName = option_text.replace(regPure, "");
  688. const price = "💳" + price_in_cents_with_discount / 100 + " " + symbol;
  689. const discount = percent_savings_text !== " " ? "🔖" + percent_savings_text + " " : "";
  690. subInfos.push([packageid, subName, discount, price]);
  691. }
  692. }
  693. }
  694. console.info(subInfos);
  695. resolve(subInfos);
  696. } else {
  697. reject(t("networkRequestError"));
  698. }
  699. }).catch((err) => {
  700. reject(err);
  701. });
  702. });
  703. }
  704. //添加购物车,只支持subID和bundleID
  705. function addCart(type = "sub", subID, appID = null) {
  706. window.localStorage["fac_subid"] = subID;
  707. return new Promise((resolve, reject) => {
  708. let data = {
  709. action: "add_to_cart",
  710. originating_snr: "1_store-navigation__",
  711. sessionid: document.cookie.replace(/(?:(?:^|.*;\s*)sessionid\s*\=\s*([^;]*).*$)|^.*$/, "$1"),
  712. snr: "1_5_9__403",
  713. }
  714. data[`${type}id`] = String(subID);
  715. let s = [];
  716. for (let k in data) {
  717. s += `${k}=${encodeURIComponent(data[k])}&`;
  718. }
  719. fetch("https://store.steampowered.com/cart/", {
  720. method: "POST",
  721. credentials: "include",
  722. body: s,
  723. headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
  724. })
  725. .then(async (response) => {
  726. if (response.ok) {
  727. let data = await response.text();
  728. if (appID !== null) {
  729. const regIfSucc = new RegExp("app\/" + appID);
  730. if (data.search(regIfSucc) !== -1) {
  731. resolve([true, t("addCartSuccess")]);
  732. }
  733. else {
  734. resolve([false, t("addCartError")]);
  735. }
  736. } else {
  737. resolve([true, t("addCartSuccess")]);
  738. }
  739. } else {
  740. resolve([false, t("networkRequestError")]);
  741. }
  742. }).catch((err) => {
  743. console.error(err);
  744. resolve([false, `${t("unknownError")}: ${err}`]);
  745. });
  746. });
  747. }
  748. //显示提示
  749. function showAlert(title, text, succ = true) {
  750. return ShowAlertDialog(`${succ ? "✅" : "❌"}${title}`, text);
  751. }
  752. })();
  753.  
  754. GM_addStyle(`
  755. button.fac_listbtns {
  756. display: none;
  757. position: relative;
  758. z-index: 100;
  759. padding: 1px;
  760. }
  761. a.search_result_row > button.fac_listbtns {
  762. top: -25px;
  763. left: 300px;
  764. }
  765. a.tab_item > button.fac_listbtns {
  766. top: -40px;
  767. left: 330px;
  768. }
  769. a.recommendation_link > button.fac_listbtns {
  770. bottom: 10px;
  771. right: 10px;
  772. position: absolute;
  773. }
  774. div.wishlist_row > button.fac_listbtns {
  775. top: 35%;
  776. right: 30%;
  777. position: absolute;
  778. }
  779. div.game_purchase_action > button.fac_listbtns {
  780. right: 8px;
  781. bottom: 8px;
  782. }
  783. button.fac_cartbtns {
  784. padding: 5px 10px;
  785. }
  786. button.fac_cartbtns:not(:last-child) {
  787. margin-right: 5px;
  788. }
  789. button.fac_cartbtns:not(:first-child) {
  790. margin-left: 5px;
  791. }
  792. a.tab_item:hover button.fac_listbtns,
  793. a.search_result_row:hover button.fac_listbtns,
  794. div.recommendation:hover button.fac_listbtns,
  795. div.wishlist_row:hover button.fac_listbtns {
  796. display: block;
  797. }
  798. div.game_purchase_action:hover > button.fac_listbtns {
  799. display: inline;
  800. }
  801. button.fac_choose {
  802. padding: 1px;
  803. margin: 2px 5px;
  804. }
  805. textarea.fac_inputbox {
  806. height: 130px;
  807. resize: vertical;
  808. font-size: 10px;
  809. min-height: 130px;
  810. }
  811. textarea.fac_diag {
  812. height: 150px;
  813. width: 600px;
  814. resize: vertical;
  815. font-size: 10px;
  816. margin-bottom: 5px;
  817. padding: 5px;
  818. background-color: rgba(0, 0, 0, 0.4);
  819. color: #fff;
  820. border: 1 px solid #000;
  821. border-radius: 3 px;
  822. box-shadow: 1px 1px 0px #45556c;
  823. }
  824. `);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址