Greasy Fork 还支持 简体中文。

Fast_Add_Cart

Add to cart without redirect to cart page, also provide import/export cart feature.

安裝腳本?
作者推薦腳本

您可能也會喜歡 Hidden_DLC_Helper

安裝腳本
  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.com/@chr233
  7. // @version 4.9
  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. // @match https://steamcommunity.com/*
  13. // @license AGPL-3.0
  14. // @icon https://blog.chrxw.com/favicon.ico
  15. // @grant GM_addStyle
  16. // @grant GM_setClipboard
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_registerMenuCommand
  20. // ==/UserScript==
  21.  
  22. (async () => {
  23. "use strict";
  24.  
  25. // 多语言
  26. const LANG = {
  27. ZH: {
  28. langName: "中文",
  29. changeLang: "修改插件语言",
  30. facInputBoxPlaceHolder:
  31. "一行一条, 自动忽略【#】后面的内容, 支持的格式如下: (自动保存)",
  32. storeLink: "商店链接",
  33. steamDBLink: "DB链接",
  34. import: "导入",
  35. importDesc: "从文本框批量添加购物车(从上到下导入)",
  36. importDesc2: "当前页面无法导入购物车",
  37. export: "导出",
  38. exportDesc: "将购物车内容导出至文本框",
  39. exportConfirm: "输入框中含有内容, 请选择操作?",
  40. exportConfirmReplace: "覆盖原有内容",
  41. exportConfirmAppend: "添加到最后",
  42. copy: "复制",
  43. copyDesc: "复制文本框中的内容",
  44. copyDone: "复制到剪贴板成功",
  45. reset: "清除",
  46. resetDesc: "清除文本框和已保存的数据",
  47. resetConfirm: "您确定要清除文本框和已保存的数据吗?",
  48. history: "历史",
  49. historyDesc: "查看购物车历史记录",
  50. reload: "刷新",
  51. reloadDesc: "重新读取保存的购物车内容",
  52. reloadConfirm: "您确定要重新读取保存的购物车数据吗?",
  53. goBack: "返回",
  54. goBackDesc: "返回你当前的购物车",
  55. clear: "清空购物车",
  56. clearDesc: "清空购物车",
  57. clearConfirm: "您确定要移除所有您购物车中的物品吗?",
  58. help: "帮助",
  59. helpDesc: "显示帮助",
  60. helpTitle: "插件版本",
  61. formatError: "格式有误",
  62. chooseSub: "请选择SUB",
  63. operation: "操作中……",
  64. operationDone: "操作完成",
  65. addCart: "添加购物车",
  66. addCartTips: "添加到购物车……",
  67. addCartErrorSubNotFount: "未识别到SubID",
  68. noSubDesc: "可能尚未发行或者是免费游戏",
  69. inCart: "在购物车中",
  70. importingTitle: "正在导入购物车……",
  71. add: "添加",
  72. toCart: "到购物车",
  73. tips: "提示",
  74. ok: "是",
  75. no: "否",
  76. fetchingSubs: "读取可用SUB",
  77. noSubFound: "未找到可用SUB",
  78. networkError: "网络错误",
  79. addCartSuccess: "添加购物车成功",
  80. addCartError: "添加购物车失败",
  81. networkRequestError: "网络请求失败",
  82. unknownError: "未知错误",
  83. unrecognizedResult: "返回了未知结果",
  84. batchExtract: "批量提取",
  85. batchExtractDone: "批量提取完成",
  86. batchDesc: "AppID已提取, 可以在购物车页批量导入",
  87. onlyOnsale: " 仅打折",
  88. onlyOnsaleDesc: "勾选后批量导入时仅导入正在打折的游戏.",
  89. onlyOnsaleDesc2: "勾选后批量导出时仅导出正在打折的游戏.",
  90. notOnSale: "尚未打折, 跳过",
  91. showUserCountry: "显示账号区域",
  92. yes: "是",
  93. },
  94. EN: {
  95. langName: "English",
  96. changeLang: "Change plugin language",
  97. facInputBoxPlaceHolder:
  98. "One line one item, ignore the content after #, support format: (auto save)",
  99. storeLink: "Store link",
  100. steamDBLink: "DB link",
  101. import: "Import",
  102. importDesc: "Batch add cart from textbox (from top to bottom)",
  103. importDesc2: "Current page can't import cart",
  104. export: "Export",
  105. exportDesc: "Export cart content to textbox",
  106. exportConfirm: "Textbox contains content, please choose operation?",
  107. exportConfirmReplace: "Replace original content",
  108. exportConfirmAppend: "Append to the end",
  109. copy: "Copy",
  110. copyDesc: "Copy textbox content",
  111. copyDone: "Copy to clipboard success",
  112. reset: "Reset",
  113. resetDesc: "Clear textbox and saved data",
  114. resetConfirm: "Are you sure to clear textbox and saved cart data?",
  115. history: "History",
  116. historyDesc: "View cart history",
  117. reload: "Reload",
  118. reloadDesc: "Reload saved cart date",
  119. reloadConfirm: "Are you sure to reload saved cart data?",
  120. goBack: "Back",
  121. goBackDesc: "Back to your cart",
  122. clear: "Clear",
  123. clearDesc: "Clear cart",
  124. clearConfirm: "Are you sure to remove all items in your cart?",
  125. help: "Help",
  126. helpDesc: "Show help",
  127. helpTitle: "Plugin Version",
  128. formatError: "Format error",
  129. chooseSub: "Please choose SUB",
  130. operation: "Operation in progress……",
  131. operationDone: "Operation done",
  132. addCart: "Add cart",
  133. addCartTips: "Adding to cart……",
  134. addCartErrorSubNotFount: "Unrecognized SubID",
  135. noSubDesc: "Maybe not released or free game",
  136. inCart: "In cart",
  137. importingTitle: "Importing cart……",
  138. add: "Add",
  139. toCart: "To cart",
  140. tips: "Tips",
  141. ok: "OK",
  142. no: "No",
  143. fetchingSubs: "Fetching available SUB",
  144. noSubFound: "No available SUB",
  145. networkError: "Network error",
  146. addCartSuccess: "Add cart success",
  147. addCartError: "Add cart failed",
  148. networkRequestError: "Network request failed",
  149. unknownError: "Unknown error",
  150. unrecognizedResult: "Returned unrecognized result",
  151. batchExtract: "Extract Items",
  152. batchExtractDone: "Batch Extract Done",
  153. batchDesc: "AppID list now saved, goto cart page to use batch import.",
  154. onlyOnsale: " Only on sale",
  155. onlyOnsaleDesc:
  156. "If checked, script will ignore games that is not on sale when import cart.",
  157. onlyOnsaleDesc2:
  158. "If checked, script will ignore games that is not on sale when export cart.",
  159. notOnSale: "Not on sale, skip",
  160. showUserCountry: "Show user country",
  161. yes: "Yes",
  162. },
  163. };
  164.  
  165. // 判断语言
  166. let language = GM_getValue("lang", "ZH");
  167. if (!language in LANG) {
  168. language = "ZH";
  169. GM_setValue("lang", language);
  170. }
  171. // 获取翻译文本
  172. const t = (key) => LANG[language][key] || key;
  173.  
  174. const showUserCountry = GM_getValue("show_user_country", false);
  175.  
  176. {
  177. // 自动弹出提示
  178. const languageTips = GM_getValue("languageTips", true);
  179. if (languageTips && language === "ZH") {
  180. if (!document.querySelector("html").lang.startsWith("zh")) {
  181. ShowConfirmDialog(
  182. "tips",
  183. "Fast add cart now support English, switch?",
  184. "Using English",
  185. "Don't show again"
  186. )
  187. .done(() => {
  188. GM_setValue("lang", "EN");
  189. GM_setValue("languageTips", false);
  190. window.location.reload();
  191. })
  192. .fail((bool) => {
  193. if (bool) {
  194. showAlert(
  195. "",
  196. "You can switch the plugin's language using TamperMonkey's menu."
  197. );
  198. GM_setValue("languageTips", false);
  199. }
  200. });
  201. }
  202. }
  203. //注册菜单
  204. GM_registerMenuCommand(`${t("changeLang")} (${t("langName")})`, () => {
  205. switch (language) {
  206. case "EN":
  207. language = "ZH";
  208. break;
  209. case "ZH":
  210. language = "EN";
  211. break;
  212. }
  213. GM_setValue("lang", language);
  214. window.location.reload();
  215. });
  216.  
  217. GM_registerMenuCommand(
  218. `${t("showUserCountry")} (${t(showUserCountry ? "yes" : "no")})`,
  219. () => {
  220. GM_setValue("show_user_country", !showUserCountry);
  221. window.location.reload();
  222. }
  223. );
  224. }
  225.  
  226. //获取商店语言和区域
  227. const { LANGUAGE: storeLanguage, COUNTRY: userCountry } = JSON.parse(
  228. document
  229. .querySelector("#application_config")
  230. ?.getAttribute("data-config") ?? "{}"
  231. );
  232. const { webapi_token: accessToken } = JSON.parse(
  233. document
  234. .querySelector("#application_config")
  235. ?.getAttribute("data-store_user_config") ?? "{}"
  236. );
  237.  
  238. const G_Objs = {};
  239.  
  240. if (userCountry && showUserCountry) {
  241. const area = document.querySelector("#header_wallet_ctn");
  242. if (area) {
  243. const span = document.createElement("span");
  244. span.textContent = `[${userCountry}]`;
  245. area.appendChild(span);
  246. }
  247. }
  248.  
  249. //初始化
  250. const pathname = window.location.pathname;
  251. if (pathname.startsWith("/cart")) {
  252. //购物车页
  253.  
  254. function genBr() {
  255. return document.createElement("br");
  256. }
  257. function genBtn(text, title, onclick) {
  258. let btn = document.createElement("button");
  259. btn.textContent = text;
  260. btn.title = title;
  261. btn.className = "btn_medium btnv6_blue_hoverfade fac_cartbtns";
  262. btn.addEventListener("click", onclick);
  263. return btn;
  264. }
  265. function genSpan(text) {
  266. let span = document.createElement("span");
  267. span.textContent = text;
  268. return span;
  269. }
  270. function genTxt(value, placeholder) {
  271. const t = document.createElement("textarea");
  272. t.className = "fac_inputbox";
  273. t.placeholder = placeholder;
  274. t.value = value;
  275. return t;
  276. }
  277. function genChk(name, title, checked = false) {
  278. const l = document.createElement("label");
  279. const i = document.createElement("input");
  280. const s = genSpan(name);
  281. i.textContent = name;
  282. i.title = title;
  283. i.type = "checkbox";
  284. i.className = "fac_checkbox";
  285. i.checked = checked;
  286. l.title = title;
  287. l.appendChild(i);
  288. l.appendChild(s);
  289. return [l, i];
  290. }
  291.  
  292. const savedCart = GM_getValue("fac_cart") ?? "";
  293. const placeHolder = [
  294. t("facInputBoxPlaceHolder"),
  295. `1. ${t("storeLink")}: https://store.steampowered.com/app/xxx`,
  296. `2. ${t("steamDBLink")}: https://steamdb.info/app/xxx`,
  297. "3. appID: xxx a/xxx app/xxx",
  298. "4. subID: s/xxx sub/xxx",
  299. "5. bundleID: b/xxx bundle/xxx",
  300. ].join("\n");
  301.  
  302. const inputBox = genTxt(savedCart, placeHolder);
  303.  
  304. function fitInputBox() {
  305. inputBox.style.height =
  306. Math.min(inputBox.value.split("\n").length * 20 + 20, 900).toString() +
  307. "px";
  308. }
  309.  
  310. inputBox.addEventListener("input", fitInputBox);
  311. G_Objs.inputBox = inputBox;
  312. fitInputBox();
  313.  
  314. const originResetBtn = document.querySelector("div.remove_ctn");
  315. if (originResetBtn != null) {
  316. originResetBtn.style.display = "none";
  317. }
  318.  
  319. const [lblDiscount, chkDiscount] = genChk(
  320. t("onlyOnsale"),
  321. t("onlyOnsaleDesc"),
  322. GM_getValue("fac_discount") ?? false
  323. );
  324. G_Objs.chkDiscount = chkDiscount;
  325.  
  326. const btnImport = genBtn(`🔼${t("import")}`, t("importDesc"), async () => {
  327. inputBox.value = await importCart(inputBox.value, chkDiscount.checked);
  328. });
  329.  
  330. const histryPage = pathname.search("history") !== -1;
  331. if (histryPage) {
  332. btnImport.disabled = true;
  333. btnImport.title = t("importDesc2");
  334. }
  335.  
  336. const [lblDiscount2, chkDiscount2] = genChk(
  337. t("onlyOnsale"),
  338. t("onlyOnsaleDesc2"),
  339. GM_getValue("fac_discount2") ?? false
  340. );
  341. G_Objs.chkDiscount2 = chkDiscount2;
  342.  
  343. const btnExport = genBtn(`🔽${t("export")}`, t("exportDesc"), async () => {
  344. let currentValue = inputBox.value.trim();
  345. const now = new Date().toLocaleString();
  346.  
  347. var data = await exportCart(chkDiscount2.checked);
  348.  
  349. if (currentValue !== "") {
  350. ShowConfirmDialog(
  351. "",
  352. t("exportConfirm"),
  353. t("exportConfirmReplace"),
  354. t("exportConfirmAppend")
  355. )
  356. .done(() => {
  357. inputBox.value = `========【${now}】=========\n` + data;
  358. fitInputBox();
  359. })
  360. .fail((bool) => {
  361. if (bool) {
  362. inputBox.value =
  363. currentValue + `\n========【${now}】=========\n` + data;
  364. fitInputBox();
  365. }
  366. });
  367. } else {
  368. inputBox.value =
  369. `========【${now}】=========\n` + exportCart(chkDiscount2.checked);
  370. fitInputBox();
  371. }
  372. });
  373.  
  374. const btnConvertToGift = genBtn("转送礼", "将购物车项目转换为送礼", () => {
  375. editCart(true, false);
  376. });
  377.  
  378. const btnConvertToSelf = genBtn(
  379. "转自用",
  380. "将购物车项目转换为为自己购买",
  381. () => {
  382. editCart(false, false);
  383. }
  384. );
  385.  
  386. const btnCopy = genBtn(`📋${t("copy")}`, t("copyDesc"), () => {
  387. GM_setClipboard(inputBox.value, "text");
  388. showAlert(t("tips"), t("copyDone"), true);
  389. });
  390. const btnClear = genBtn(`🗑️${t("reset")}`, t("resetDesc"), () => {
  391. ShowConfirmDialog("", t("resetConfirm"), t("ok"), t("no")).done(() => {
  392. inputBox.value = "";
  393. GM_setValue("fac_cart", "");
  394. fitInputBox();
  395. });
  396. });
  397. const btnReload = genBtn(`🔃${t("reload")}`, t("reloadDesc"), () => {
  398. ShowConfirmDialog("", t("reloadConfirm"), t("ok"), t("no")).done(() => {
  399. const s = GM_getValue("fac_cart") ?? "";
  400. inputBox.value = s;
  401. fitInputBox();
  402. });
  403. });
  404. const btnHistory = genBtn(`📜${t("history")}`, t("historyDesc"), () => {
  405. window.location.href =
  406. "https://help.steampowered.com/zh-cn/accountdata/ShoppingCartHistory";
  407. });
  408. const btnBack = genBtn(`↩️${t("goBack")}`, t("goBackDesc"), () => {
  409. window.location.href = "https://store.steampowered.com/cart/";
  410. });
  411. const btnForget = genBtn(`⚠️${t("clear")}`, t("clearDesc"), () => {
  412. ShowConfirmDialog("", t("clearConfirm"), t("ok"), t("no")).done(() => {
  413. deleteAccountCart()
  414. .then(() => {
  415. location.reload();
  416. })
  417. .catch((err) => {
  418. console.error(err);
  419. showAlert("出错", err, false);
  420. });
  421. });
  422. });
  423. const btnHelp = genBtn(`🔣${t("help")}`, t("helpDesc"), () => {
  424. const {
  425. script: { version },
  426. } = GM_info;
  427. showAlert(
  428. `${t("helpTitle")} ${version}`,
  429. [
  430. `<p>【🔼${t("import")}】${t("importDesc")}</p>`,
  431. `<p>【✅${t("onlyOnsale")}】${t("onlyOnsaleDesc")}</p>`,
  432. `<p>【🔽${t("export")}】${t("exportDesc")}</p>`,
  433. `<p>【✅${t("onlyOnsale")}】${t("onlyOnsaleDesc2")}</p>`,
  434. `<p>【📋${t("copy")}】${t("copyDesc")}</p>`,
  435. `<p>【🗑️${t("reset")}】${t("resetDesc")}。</p>`,
  436. `<p>【📜${t("history")}】${t("historyDesc")}</p>`,
  437. `<p>【↩️${t("goBack")}】${t("goBackDesc")}</p>`,
  438. `<p>【⚠️${t("clear")}】${t("clearDesc")}</p>`,
  439. `<p>【🔣${t("help")}】${t("helpDesc")}</p>`,
  440. `<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>`,
  441. ].join("<br>"),
  442. true
  443. );
  444. });
  445.  
  446. const btnArea = document.createElement("div");
  447. btnArea.appendChild(btnImport);
  448. // btnArea.appendChild(btnImport2);
  449. btnArea.appendChild(lblDiscount);
  450. btnArea.appendChild(genSpan(" | "));
  451. btnArea.appendChild(btnExport);
  452. btnArea.appendChild(lblDiscount2);
  453. btnArea.appendChild(genSpan(" | "));
  454. btnArea.appendChild(btnConvertToGift);
  455. btnArea.appendChild(btnConvertToSelf);
  456. btnArea.appendChild(genSpan(" | "));
  457. btnArea.appendChild(btnHelp);
  458.  
  459. const btnArea2 = document.createElement("div");
  460. btnArea2.appendChild(btnCopy);
  461. btnArea2.appendChild(btnClear);
  462. btnArea2.appendChild(btnReload);
  463. btnArea2.appendChild(genSpan(" | "));
  464. btnArea2.appendChild(histryPage ? btnBack : btnHistory);
  465. btnArea2.appendChild(genSpan(" | "));
  466. btnArea2.appendChild(btnForget);
  467.  
  468. const fastAddCartPanel = document.createElement("div");
  469. fastAddCartPanel.className = "fac_panel";
  470.  
  471. fastAddCartPanel.appendChild(btnArea);
  472. fastAddCartPanel.appendChild(genBr());
  473. fastAddCartPanel.appendChild(inputBox);
  474. fastAddCartPanel.appendChild(genBr());
  475. fastAddCartPanel.appendChild(btnArea2);
  476.  
  477. window.addEventListener("beforeunload", () => {
  478. GM_setValue("fac_cart", inputBox.value);
  479. GM_setValue("fac_discount", chkDiscount.checked);
  480. GM_setValue("fac_discount2", chkDiscount2.checked);
  481. });
  482.  
  483. //等待购物车加载完毕, 显示额外面板
  484. const timer = setInterval(() => {
  485. const container = document.querySelector(
  486. "div[data-featuretarget='react-root']>div>div:last-child>div:last-child>div:last-child>div:nth-child(1)>div:last-child"
  487. );
  488. if (container) {
  489. clearInterval(timer);
  490.  
  491. container.parentElement.insertBefore(fastAddCartPanel, container);
  492. }
  493. }, 500);
  494. }
  495.  
  496. // getStoreItem([730], null, null).then((data) => console.log(data)).catch(err => console.error(err))
  497. // getAccountCart().then((data) => console.log(data)).catch(err => console.error(err))
  498. // addItemsToAccountCart(null, [28627]).then((data) => console.log(data)).catch(err => console.error(err))
  499.  
  500. //始终在右上角显示购物车按钮
  501. const cart_btn = document.getElementById("store_header_cart_btn");
  502. if (cart_btn !== null) {
  503. cart_btn.style.display = "";
  504. }
  505.  
  506. //导入购物车
  507. function importCart(text, onlyOnSale = false) {
  508. const regFull = new RegExp(/(app|a|bundle|b|sub|s)\/(\d+)/);
  509. const regShort = new RegExp(/^([\s]*|)(\d+)/);
  510.  
  511. return new Promise(async (resolve, reject) => {
  512. const dialog = showAlert(
  513. "导入购物车",
  514. `<h2 id="fac_diag" class="fac_diag">${t("operation")}</h2>`,
  515. true
  516. );
  517.  
  518. const timer = setInterval(async () => {
  519. let txt = document.getElementById("fac_diag");
  520. if (txt) {
  521. clearInterval(timer);
  522.  
  523. const txts = text.split("\n");
  524.  
  525. const result = [];
  526.  
  527. const appIds = [];
  528. const subIds = [];
  529. const bundleIds = [];
  530.  
  531. const targetSubIds = [];
  532. const targetBundleIds = [];
  533.  
  534. try {
  535. txt.textContent = "0/4 开始读取输入信息";
  536.  
  537. for (let line of txts) {
  538. if (line.trim() === "") {
  539. continue;
  540. }
  541. const tmp = line.split("#")[0];
  542.  
  543. const match = line.match(regFull) ?? line.match(regShort);
  544. if (!match) {
  545. if (line.search("=====") === -1) {
  546. result.push(`${tmp} #${t("formatError")}`);
  547. } else {
  548. result.push(line);
  549. }
  550. continue;
  551. }
  552.  
  553. let [_, type, subID] = match;
  554. subID = parseInt(subID);
  555. if (subID !== subID) {
  556. result.push(`${tmp} #${t("formatError")}`);
  557. continue;
  558. }
  559.  
  560. switch (type.toLowerCase()) {
  561. case "":
  562. case "a":
  563. case "app":
  564. type = "app";
  565. appIds.push(subID);
  566. break;
  567. case "s":
  568. case "sub":
  569. type = "sub";
  570. subIds.push(subID);
  571. break;
  572. case "b":
  573. case "bundle":
  574. type = "bundle";
  575. bundleIds.push(subID);
  576. break;
  577. default:
  578. result.push(`${tmp} #${t("formatError")}`);
  579. continue;
  580. }
  581. }
  582.  
  583. const count = appIds.length + subIds.length + bundleIds.length;
  584. txt.textContent = `1/4 成功读取 ${count} 个输入内容`;
  585.  
  586. if (count > 0) {
  587. txt.textContent = "1/4 开始读取游戏信息";
  588. const store_items = await getStoreItem(appIds, subIds, bundleIds);
  589.  
  590. console.log(store_items);
  591.  
  592. txt.textContent = "2/4 读取游戏信息成功";
  593.  
  594. for (let { appid, purchase_options } of store_items) {
  595. if (!purchase_options) {
  596. continue;
  597. }
  598.  
  599. //输入值包含AppId, 解析可购买项
  600. if (appIds.includes(appid)) {
  601. if (purchase_options.length >= 1) {
  602. const {
  603. packageid,
  604. bundleid,
  605. purchase_option_name: name,
  606. discount_pct: discount,
  607. formatted_final_price: price,
  608. } = purchase_options[0];
  609. if (discount) {
  610. if (packageid) {
  611. result.push(
  612. `sub/${packageid} #app/${appid} #${name} 💳 ${price} 🔖 ${discount}`
  613. );
  614. targetSubIds.push(packageid);
  615. } else if (bundleid) {
  616. result.push(
  617. `bundle/${bundleid} #app/${appid} #${name} 💳 ${price} 🔖 ${discount}`
  618. );
  619. targetBundleIds.push(bundleid);
  620. }
  621. } else {
  622. if (packageid) {
  623. if (!onlyOnSale) {
  624. result.push(
  625. `sub/${packageid} #app/${appid} #${name} 💳 ${price}`
  626. );
  627. targetSubIds.push(packageid);
  628. } else {
  629. result.push(
  630. `sub/${packageid} #app/${appid} #排除 #${name} 💳 ${price}`
  631. );
  632. }
  633. } else if (bundleid) {
  634. if (!onlyOnSale) {
  635. result.push(
  636. `bundle/${bundleid} #app/${appid} #${name} 💳 ${price}`
  637. );
  638. targetBundleIds.push(bundleid);
  639. } else {
  640. result.push(
  641. `bundle/${bundleid} #app/${appid} #排除 #${name} 💳 ${price}`
  642. );
  643. }
  644. }
  645. }
  646. } else {
  647. result.push(`${tmp} #无可购买项目`);
  648. }
  649. }
  650.  
  651. for (let {
  652. packageid,
  653. bundleid,
  654. purchase_option_name: name,
  655. discount_pct: discount,
  656. formatted_final_price: price,
  657. } of purchase_options) {
  658. if (discount) {
  659. if (packageid && subIds.includes(packageid)) {
  660. result.push(
  661. `sub/${packageid} #${name} 💳 ${price} 🔖 ${discount}`
  662. );
  663. targetSubIds.push(packageid);
  664. } else if (bundleid && bundleIds.includes(bundleid)) {
  665. result.push(
  666. `bundle/${bundleid} #${name} 💳 ${price} 🔖 ${discount}`
  667. );
  668. targetBundleIds.push(bundleid);
  669. }
  670. } else {
  671. if (packageid && subIds.includes(packageid)) {
  672. if (!onlyOnSale) {
  673. result.push(`sub/${packageid} #${name} 💳 ${price}`);
  674. targetSubIds.push(packageid);
  675. } else {
  676. result.push(
  677. `sub/${packageid} #排除 #${name} 💳 ${price}`
  678. );
  679. }
  680. } else if (bundleid && bundleIds.includes(bundleid)) {
  681. if (!onlyOnSale) {
  682. result.push(`bundle/${bundleid} #${name} 💳 ${price}`);
  683. targetBundleIds.push(bundleid);
  684. } else {
  685. result.push(
  686. `bundle/${bundleid} #排除 #${name} 💳 ${price}`
  687. );
  688. }
  689. }
  690. }
  691. }
  692. }
  693.  
  694. txt.textContent = "3/4 解析游戏信息成功";
  695.  
  696. const data = await addItemsToAccountCart(
  697. targetSubIds,
  698. targetBundleIds,
  699. false
  700. );
  701. console.log(data);
  702.  
  703. txt.textContent = "4/4 导入购物车成功";
  704.  
  705. dialog.Dismiss();
  706.  
  707. resolve(result.join("\n"));
  708.  
  709. setTimeout(() => {
  710. window.location.reload();
  711. }, 1000);
  712. } else {
  713. txt.textContent = "4/4 尚未输入有效内容";
  714. resolve(result.join("\n"));
  715. }
  716. } catch (err) {
  717. txt.textContent = "导出购物车失败";
  718. console.error(err);
  719. resolve(result.join("\n"));
  720. }
  721. }
  722. }, 200);
  723. });
  724. }
  725. //导出购物车
  726. function exportCart(onlyOnsale = false) {
  727. return new Promise(async (resolve, reject) => {
  728. const dialog = showAlert(
  729. "导出购物车",
  730. `<h2 id="fac_diag" class="fac_diag">${t("operation")}</h2>`,
  731. true
  732. );
  733.  
  734. const timer = setInterval(async () => {
  735. let txt = document.getElementById("fac_diag");
  736. if (txt) {
  737. clearInterval(timer);
  738.  
  739. const result = [];
  740.  
  741. const subIds = [];
  742. const bundleIds = [];
  743. const gameNames = {};
  744.  
  745. try {
  746. txt.textContent = "0/4 开始读取账号购物车";
  747.  
  748. const { line_items } = await getAccountCart();
  749.  
  750. if (line_items) {
  751. for (let { packageid, bundleid } of line_items) {
  752. if (packageid) {
  753. subIds.push(packageid);
  754. } else if (bundleid) {
  755. bundleIds.push(bundleid);
  756. }
  757. }
  758. }
  759.  
  760. const count = subIds.length + bundleIds.length;
  761. txt.textContent = `1/4 成功读取 ${count} 个购物车内容`;
  762.  
  763. if (count > 0) {
  764. txt.textContent = "1/4 开始读取游戏信息";
  765. const store_items = await getStoreItem(null, subIds, bundleIds);
  766. txt.textContent = "2/4 读取游戏信息成功";
  767.  
  768. for (let { purchase_options } of store_items) {
  769. if (!purchase_options) {
  770. continue;
  771. }
  772.  
  773. for (let {
  774. packageid,
  775. bundleid,
  776. purchase_option_name,
  777. discount_pct,
  778. } of purchase_options) {
  779. let key;
  780. if (packageid) {
  781. key = `sub/${packageid}`;
  782. } else if (bundleid) {
  783. key = `bundle/${bundleid}`;
  784. } else {
  785. continue;
  786. }
  787. gameNames[key] = [`${purchase_option_name}`, discount_pct];
  788. }
  789. }
  790.  
  791. txt.textContent = "3/4 解析游戏信息成功";
  792. txt.textContent = "3/4 开始导出购物车信息";
  793. if (line_items) {
  794. for (let {
  795. packageid,
  796. bundleid,
  797. price_when_added: { formatted_amount },
  798. } of line_items) {
  799. let key;
  800. if (packageid) {
  801. key = `sub/${packageid}`;
  802. } else if (bundleid) {
  803. key = `bundle/${bundleid}`;
  804. }
  805. const [name, discount] = gameNames[key] ?? "_";
  806. if (discount) {
  807. result.push(
  808. `${key} #${name} 💳 ${formatted_amount} 🔖 ${discount}`
  809. );
  810. } else if (!onlyOnsale) {
  811. result.push(`${key} #${name} 💳 ${formatted_amount}`);
  812. }
  813. }
  814. }
  815.  
  816. txt.textContent = "3/4 导出购物车信息成功";
  817. dialog.Dismiss();
  818.  
  819. resolve(result.join("\n"));
  820. } else {
  821. txt.textContent = "4/4 购物车内容为空";
  822. resolve(result.join("\n"));
  823. }
  824. } catch (err) {
  825. txt.textContent = "读取账号购物车失败";
  826. console.error(err);
  827. resolve(result.join("\n"));
  828. }
  829. }
  830. }, 200);
  831. });
  832. }
  833.  
  834. //编辑购物车
  835. async function editCart(setToGift = false, setToPrivate = false) {
  836. const setGiftInfo = await inputGiftee(setToGift);
  837.  
  838. return new Promise(async (resolve, reject) => {
  839. const dialog = showAlert(
  840. "编辑购物车",
  841. `<h2 id="fac_diag" class="fac_diag">${t("operation")}</h2>`,
  842. true
  843. );
  844.  
  845. const timer = setInterval(async () => {
  846. let txt = document.getElementById("fac_diag");
  847. if (txt) {
  848. clearInterval(timer);
  849.  
  850. const lineItemIds = [];
  851.  
  852. try {
  853. txt.textContent = "0/3 开始读取账号购物车";
  854.  
  855. const { line_items } = await getAccountCart();
  856.  
  857. if (line_items) {
  858. for (const {
  859. line_item_id,
  860. flags: { is_gift, is_private },
  861. gift_info,
  862. } of line_items) {
  863. const accountid_giftee =
  864. gift_info?.accountid_giftee?.toString() ?? "";
  865.  
  866. console.log(
  867. line_item_id,
  868. is_gift,
  869. is_private,
  870. accountid_giftee
  871. );
  872.  
  873. //跳过无需处理的id
  874. if (setToGift) {
  875. if (
  876. is_gift &&
  877. setGiftInfo?.accountid_giftee == accountid_giftee
  878. ) {
  879. continue;
  880. }
  881. } else if (setToPrivate) {
  882. if (is_private) {
  883. continue;
  884. }
  885. } else {
  886. if (!is_gift && !is_private) {
  887. continue;
  888. }
  889. }
  890.  
  891. lineItemIds.push(line_item_id);
  892. }
  893. }
  894.  
  895. console.log(lineItemIds);
  896.  
  897. const count = lineItemIds.length;
  898. txt.textContent = `1/3 共计 ${count} 个待修改购物车内容`;
  899.  
  900. if (count > 0) {
  901. for (let i = 0; i < count; i++) {
  902. const itemId = lineItemIds[i];
  903. await editAccountCart(
  904. itemId,
  905. setToGift,
  906. setToPrivate,
  907. setGiftInfo
  908. );
  909. txt.textContent = `2/3 当前进度 ${i + 1} / ${count}`;
  910. }
  911.  
  912. txt.textContent = "3/3 批量修改购物车成功";
  913.  
  914. setTimeout(() => dialog.Dismiss(), 1000);
  915. resolve();
  916. } else {
  917. txt.textContent = "3/3 购物车无需修改";
  918.  
  919. setTimeout(() => dialog.Dismiss(), 1000);
  920. resolve();
  921. }
  922. } catch (err) {
  923. txt.textContent = "读取账号购物车失败";
  924. console.error(err);
  925. resolve();
  926. }
  927. }
  928. }, 200);
  929. });
  930. }
  931.  
  932. const steamIdConvert = BigInt("0x110000100000000");
  933.  
  934. function inputGiftee(isGift = false) {
  935. const nickname =
  936. document.querySelector("#account_pulldown")?.textContent?.trim() ??
  937. "unknown";
  938.  
  939. return new Promise((resolve, reject) => {
  940. if (!isGift) {
  941. resolve(null);
  942. } else {
  943. ShowPromptDialog(
  944. "提示",
  945. "请输入礼物接收人的 SteamID 或者好友代码, 可以留空",
  946. "确认",
  947. "跳过"
  948. )
  949. .done((text) => {
  950. try {
  951. let steamId = BigInt(text.trim());
  952. if (steamId == 0) {
  953. throw "输入数值有误";
  954. }
  955.  
  956. if (steamId > steamIdConvert) {
  957. steamId -= steamIdConvert;
  958. }
  959. const giftInfo = {
  960. accountid_giftee: steamId.toString(),
  961. gift_message: {
  962. gifteename: steamId.toString(),
  963. message: "Send by Fast_Add_Cart",
  964. sentiment: nickname,
  965. signature: nickname,
  966. },
  967. time_scheduled_send: 0,
  968. };
  969. resolve(giftInfo);
  970. } catch (err) {
  971. ShowAlertDialog("提示", "输入数值有误").then(() => resolve(null));
  972. }
  973. })
  974. .fail(() => {
  975. resolve(null);
  976. });
  977. }
  978. });
  979. }
  980.  
  981. // 获取游戏详情
  982. function getStoreItem(appIds = null, subIds = null, bundleIds = null) {
  983. return new Promise((resolve, reject) => {
  984. const ids = [];
  985. if (appIds) {
  986. for (let x of appIds) {
  987. ids.push({ appid: x });
  988. }
  989. }
  990. if (subIds) {
  991. for (let x of subIds) {
  992. ids.push({ packageid: x });
  993. }
  994. }
  995. if (bundleIds) {
  996. for (let x of bundleIds) {
  997. ids.push({ bundleid: x });
  998. }
  999. }
  1000.  
  1001. if (ids.length === 0) {
  1002. reject([false, "未提供有效ID"]);
  1003. }
  1004.  
  1005. const payload = {
  1006. ids,
  1007. context: {
  1008. language: storeLanguage,
  1009. country_code: userCountry,
  1010. steam_realm: "1",
  1011. },
  1012. data_request: {
  1013. include_all_purchase_options: true,
  1014. },
  1015. };
  1016. const json = encodeURI(JSON.stringify(payload));
  1017. fetch(
  1018. `https://api.steampowered.com/IStoreBrowseService/GetItems/v1/?input_json=${json}`,
  1019. {
  1020. method: "GET",
  1021. }
  1022. )
  1023. .then(async (response) => {
  1024. if (response.ok) {
  1025. const {
  1026. response: { store_items },
  1027. } = await response.json();
  1028. resolve(store_items);
  1029. } else {
  1030. reject(t("networkRequestError"));
  1031. }
  1032. })
  1033. .catch((err) => {
  1034. reject(err);
  1035. });
  1036. });
  1037. }
  1038.  
  1039. //读取购物车
  1040. function getAccountCart() {
  1041. return new Promise((resolve, reject) => {
  1042. fetch(
  1043. `https://api.steampowered.com/IAccountCartService/GetCart/v1/?access_token=${accessToken}`,
  1044. {
  1045. method: "GET",
  1046. }
  1047. )
  1048. .then(async (response) => {
  1049. if (response.ok) {
  1050. const {
  1051. response: { cart },
  1052. } = await response.json();
  1053. resolve(cart);
  1054. } else {
  1055. reject(t("networkRequestError"));
  1056. }
  1057. })
  1058. .catch((err) => {
  1059. reject(err);
  1060. });
  1061. });
  1062. }
  1063.  
  1064. //添加购物车
  1065. function addItemsToAccountCart(
  1066. subIds = null,
  1067. bundleIds = null,
  1068. isPrivate = false
  1069. ) {
  1070. return new Promise((resolve, reject) => {
  1071. const items = [];
  1072. if (subIds) {
  1073. for (let x of subIds) {
  1074. items.push({ packageid: x });
  1075. }
  1076. }
  1077. if (bundleIds) {
  1078. for (let x of bundleIds) {
  1079. items.push({ bundleid: x });
  1080. }
  1081. }
  1082. if (items.length === 0) {
  1083. reject([false, "未提供有效ID"]);
  1084. }
  1085.  
  1086. for (let x of items) {
  1087. x["gift_info"] = null; //giftInfo;
  1088. x["flags"] = {
  1089. is_gift: false,
  1090. is_private: isPrivate == true,
  1091. };
  1092. }
  1093.  
  1094. const payload = {
  1095. user_country: userCountry,
  1096. items,
  1097. navdata: {
  1098. domain: "store.steampowered.com",
  1099. controller: "default",
  1100. method: "default",
  1101. submethod: "",
  1102. feature: "spotlight",
  1103. depth: 1,
  1104. countrycode: userCountry,
  1105. webkey: 0,
  1106. is_client: false,
  1107. curator_data: {
  1108. clanid: null,
  1109. listid: null,
  1110. },
  1111. is_likely_bot: false,
  1112. is_utm: false,
  1113. },
  1114. };
  1115. const json = JSON.stringify(payload);
  1116.  
  1117. fetch(
  1118. `https://api.steampowered.com/IAccountCartService/AddItemsToCart/v1/?access_token=${accessToken}`,
  1119. {
  1120. method: "POST",
  1121. body: `input_json=${json}`,
  1122. headers: {
  1123. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  1124. },
  1125. }
  1126. )
  1127. .then(async (response) => {
  1128. if (response.ok) {
  1129. const {
  1130. response: { cart },
  1131. } = await response.json();
  1132. resolve(cart);
  1133. } else {
  1134. reject(t("networkRequestError"));
  1135. }
  1136. })
  1137. .catch((err) => {
  1138. reject(err);
  1139. });
  1140. });
  1141. }
  1142.  
  1143. //编辑购物车
  1144. function editAccountCart(itemId, isGift, isPrivate, giftInfo = null) {
  1145. return new Promise((resolve, reject) => {
  1146. const payload = {
  1147. line_item_id: itemId,
  1148. user_country: userCountry,
  1149. gift_info: giftInfo,
  1150. flags: {
  1151. is_gift: isGift,
  1152. is_private: isPrivate,
  1153. },
  1154. };
  1155. const json = JSON.stringify(payload);
  1156.  
  1157. console.log(json);
  1158.  
  1159. fetch(
  1160. `https://api.steampowered.com/IAccountCartService/ModifyLineItem/v1/?access_token=${accessToken}`,
  1161. {
  1162. method: "POST",
  1163. body: `input_json=${json}`,
  1164. headers: {
  1165. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  1166. },
  1167. }
  1168. )
  1169. .then(async (response) => {
  1170. if (response.ok) {
  1171. const {
  1172. response: { line_item_ids, cart },
  1173. } = await response.json();
  1174. resolve([line_item_ids, cart]);
  1175. } else {
  1176. reject(t("networkRequestError"));
  1177. }
  1178. })
  1179. .catch((err) => {
  1180. reject(err);
  1181. });
  1182. });
  1183. }
  1184.  
  1185. //删除购物车
  1186. function deleteAccountCart() {
  1187. return new Promise((resolve, reject) => {
  1188. fetch(
  1189. `https://api.steampowered.com/IAccountCartService/DeleteCart/v1/?access_token=${accessToken}`,
  1190. {
  1191. method: "POST",
  1192. }
  1193. )
  1194. .then(async (response) => {
  1195. if (response.ok) {
  1196. const {
  1197. response: { line_item_ids, cart },
  1198. } = await response.json();
  1199. resolve([line_item_ids, cart]);
  1200. } else {
  1201. reject(t("networkRequestError"));
  1202. }
  1203. })
  1204. .catch((err) => {
  1205. reject(err);
  1206. });
  1207. });
  1208. }
  1209.  
  1210. //显示提示
  1211. function showAlert(title, text, succ = true) {
  1212. return ShowAlertDialog(`${succ ? "✅" : "❌"}${title}`, text);
  1213. }
  1214. })();
  1215.  
  1216. GM_addStyle(`
  1217. button.fac_listbtns {
  1218. display: none;
  1219. position: relative;
  1220. z-index: 100;
  1221. padding: 1px;
  1222. }
  1223. a.search_result_row > button.fac_listbtns {
  1224. top: -25px;
  1225. left: 300px;
  1226. }
  1227. a.tab_item > button.fac_listbtns {
  1228. top: -40px;
  1229. left: 330px;
  1230. }
  1231. a.recommendation_link > button.fac_listbtns {
  1232. bottom: 10px;
  1233. right: 10px;
  1234. position: absolute;
  1235. }
  1236. div.wishlist_row > button.fac_listbtns {
  1237. top: 35%;
  1238. right: 30%;
  1239. position: absolute;
  1240. }
  1241. div.game_purchase_action > button.fac_listbtns {
  1242. right: 8px;
  1243. bottom: 8px;
  1244. }
  1245. button.fac_cartbtns {
  1246. padding: 5px 8px;
  1247. }
  1248. button.fac_cartbtns:not(:last-child) {
  1249. margin-right: 4px;
  1250. }
  1251. button.fac_cartbtns:not(:first-child) {
  1252. margin-left: 4px;
  1253. }
  1254. a.tab_item:hover button.fac_listbtns,
  1255. a.search_result_row:hover button.fac_listbtns,
  1256. div.recommendation:hover button.fac_listbtns,
  1257. div.wishlist_row:hover button.fac_listbtns {
  1258. display: block;
  1259. }
  1260. div.game_purchase_action:hover > button.fac_listbtns {
  1261. display: inline;
  1262. }
  1263. button.fac_choose {
  1264. padding: 1px;
  1265. margin: 2px 5px;
  1266. }
  1267. textarea.fac_inputbox {
  1268. resize: vertical;
  1269. font-size: 12px;
  1270. min-height: 130px;
  1271. width: 98%;
  1272. background: #3d4450;
  1273. color: #fff;
  1274. padding: 1%;
  1275. border: gray;
  1276. border-radius: 5px;
  1277. }
  1278. `);