Steam快速添加购物车

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

当前为 2023-04-07 提交的版本,查看 最新版本

  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.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. // @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. "一行一条, 自动忽略【#】后面的内容, 支持的格式如下: (自动保存)",
  31. storeLink: "商店链接",
  32. steamDBLink: "DB链接",
  33. import: "导入(正序)",
  34. importDesc: "从文本框批量添加购物车(从上到下导入)",
  35. importDesc2: "当前页面无法导入购物车",
  36. importReverse: "导入(倒序)",
  37. importDescReverse: "从文本框批量添加购物车(从下到上导入)",
  38. export: "导出",
  39. exportDesc: "将购物车内容导出至文本框",
  40. exportConfirm: "输入框中含有内容, 请选择操作?",
  41. exportConfirmReplace: "覆盖原有内容",
  42. exportConfirmAppend: "添加到最后",
  43. copy: "复制",
  44. copyDesc: "复制文本框中的内容",
  45. copyDone: "复制到剪贴板成功",
  46. reset: "清除",
  47. resetDesc: "清除文本框和已保存的数据",
  48. resetConfirm: "您确定要清除文本框和已保存的数据吗?",
  49. history: "购物车历史",
  50. historyDesc: "查看购物车历史记录",
  51. reload: "刷新",
  52. reloadDesc: "重新读取保存的购物车内容",
  53. reloadConfirm: "您确定要重新读取保存的购物车数据吗?",
  54. goBack: "返回",
  55. goBackDesc: "返回你当前的购物车",
  56. clear: "清空购物车",
  57. clearDesc: "清空购物车",
  58. clearConfirm: "您确定要移除所有您购物车中的物品吗?",
  59. help: "帮助",
  60. helpDesc: "显示帮助",
  61. helpTitle: "插件版本",
  62. formatError: "格式有误",
  63. chooseSub: "请选择SUB",
  64. operation: "操作中……",
  65. operationDone: "操作完成",
  66. addCart: "添加购物车",
  67. addCartTips: "添加到购物车……",
  68. addCartErrorSubNotFount: "未识别到SubID",
  69. noSubDesc: "可能尚未发行或者是免费游戏",
  70. inCart: "在购物车中",
  71. importingTitle: "正在导入购物车……",
  72. add: "添加",
  73. toCart: "到购物车",
  74. tips: "提示",
  75. ok: "是",
  76. no: "否",
  77. fetchingSubs: "读取可用SUB",
  78. noSubFound: "未找到可用SUB",
  79. networkError: "网络错误",
  80. addCartSuccess: "添加购物车成功",
  81. addCartError: "添加购物车失败",
  82. networkRequestError: "网络请求失败",
  83. unknownError: "未知错误",
  84. unrecognizedResult: "返回了未知结果",
  85. batchExtract: "批量提取",
  86. batchExtractDone: "批量提取完成",
  87. batchDesc: "AppID已提取, 可以在购物车页批量导入",
  88. onlyOnsale: " 仅打折",
  89. onlyOnsaleDesc: "勾选后批量导入时仅导入正在打折的游戏.",
  90. onlyOnsaleDesc2: "勾选后批量导出时仅导出正在打折的游戏.",
  91. notOnSale: "尚未打折, 跳过",
  92. },
  93. EN: {
  94. langName: "English",
  95. changeLang: "Change plugin language",
  96. facInputBoxPlaceHolder:
  97. "One line one item, ignore the content after #, support format: (auto save)",
  98. storeLink: "Store link",
  99. steamDBLink: "DB link",
  100. import: "Import(Asc)",
  101. importDesc: "Batch add cart from textbox (from top to bottom)",
  102. importDesc2: "Current page can't import cart",
  103. importReverse: "Import(Desc)",
  104. importDescReverse: "Batch add cart from textbox (from bottom to top)",
  105. export: "Export",
  106. exportDesc: "Export cart content to textbox",
  107. exportConfirm: "Textbox contains content, please choose operation?",
  108. exportConfirmReplace: "Replace original content",
  109. exportConfirmAppend: "Append to the end",
  110. copy: "Copy",
  111. copyDesc: "Copy textbox content",
  112. copyDone: "Copy to clipboard success",
  113. reset: "Reset",
  114. resetDesc: "Clear textbox and saved data",
  115. resetConfirm: "Are you sure to clear textbox and saved cart data?",
  116. history: "History",
  117. historyDesc: "View cart history",
  118. reload: "Reload",
  119. reloadDesc: "Reload saved cart date",
  120. reloadConfirm: "Are you sure to reload saved cart data?",
  121. goBack: "Back",
  122. goBackDesc: "Back to your cart",
  123. clear: "Clear",
  124. clearDesc: "Clear cart",
  125. clearConfirm: "Are you sure to remove all items in your cart?",
  126. help: "Help",
  127. helpDesc: "Show help",
  128. helpTitle: "Plugin Version",
  129. formatError: "Format error",
  130. chooseSub: "Please choose SUB",
  131. operation: "Operation in progress……",
  132. operationDone: "Operation done",
  133. addCart: "Add cart",
  134. addCartTips: "Adding to cart……",
  135. addCartErrorSubNotFount: "Unrecognized SubID",
  136. noSubDesc: "Maybe not released or free game",
  137. inCart: "In cart",
  138. importingTitle: "Importing cart……",
  139. add: "Add",
  140. toCart: "To cart",
  141. tips: "Tips",
  142. ok: "OK",
  143. no: "No",
  144. fetchingSubs: "Fetching available SUB",
  145. noSubFound: "No available SUB",
  146. networkError: "Network error",
  147. addCartSuccess: "Add cart success",
  148. addCartError: "Add cart failed",
  149. networkRequestError: "Network request failed",
  150. unknownError: "Unknown error",
  151. unrecognizedResult: "Returned unrecognized result",
  152. batchExtract: "Extract Items",
  153. batchExtractDone: "Batch Extract Done",
  154. batchDesc: "AppID list now saved, goto cart page to use batch import.",
  155. onlyOnsale: " Only on sale",
  156. onlyOnsaleDesc:
  157. "If checked, script will ignore games that is not on sale when import cart.",
  158. onlyOnsaleDesc2:
  159. "If checked, script will ignore games that is not on sale when export cart.",
  160. notOnSale: "Not on sale, skip",
  161. },
  162. };
  163.  
  164. // 判断语言
  165. let language = GM_getValue("lang", "ZH");
  166. if (!language in LANG) {
  167. language = "ZH";
  168. GM_setValue("lang", language);
  169. }
  170. // 获取翻译文本
  171. function t(key) {
  172. return LANG[language][key] || key;
  173. }
  174. {
  175. // 自动弹出提示
  176. const languageTips = GM_getValue("languageTips", true);
  177. if (languageTips && language === "ZH") {
  178. if (!document.querySelector("html").lang.startsWith("zh")) {
  179. ShowConfirmDialog(
  180. "tips",
  181. "Fast add cart now support English, switch?",
  182. "Using English",
  183. "Don't show again"
  184. )
  185. .done(() => {
  186. GM_setValue("lang", "EN");
  187. GM_setValue("languageTips", false);
  188. window.location.reload();
  189. })
  190. .fail((bool) => {
  191. if (bool) {
  192. showAlert(
  193. "",
  194. "You can switch the plugin's language using TamperMonkey's menu."
  195. );
  196. GM_setValue("languageTips", false);
  197. }
  198. });
  199. }
  200. }
  201. }
  202. GM_registerMenuCommand(`${t("changeLang")} (${t("langName")})`, () => {
  203. switch (language) {
  204. case "EN":
  205. language = "ZH";
  206. break;
  207. case "ZH":
  208. language = "EN";
  209. break;
  210. }
  211. GM_setValue("lang", language);
  212. window.location.reload();
  213. });
  214. //初始化
  215. const pathname = window.location.pathname;
  216. if (
  217. pathname === "/search/" ||
  218. pathname === "/" ||
  219. pathname.startsWith("/tags/")
  220. ) {
  221. //搜索页,主页,标签页
  222. let timer = setInterval(() => {
  223. let containers = document.querySelectorAll(
  224. [
  225. "#search_resultsRows",
  226. "#tab_newreleases_content",
  227. "#tab_topsellers_content",
  228. "#tab_upcoming_content",
  229. "#tab_specials_content",
  230. "#NewReleasesRows",
  231. "#TopSellersRows",
  232. "#ConcurrentUsersRows",
  233. "#TopRatedRows",
  234. "#ComingSoonRows",
  235. ].join(",")
  236. );
  237. if (containers.length > 0) {
  238. for (let container of containers) {
  239. clearInterval(timer);
  240. for (let ele of container.children) {
  241. addButton(ele);
  242. }
  243. container.addEventListener("DOMNodeInserted", ({ relatedNode }) => {
  244. if (relatedNode.parentElement === container) {
  245. addButton(relatedNode);
  246. }
  247. });
  248. }
  249.  
  250. const searchBar = document.querySelector(".searchbar>.searchbar_left");
  251. if (searchBar !== null) {
  252. let btn = document.createElement("button");
  253. btn.addEventListener(
  254. "click",
  255. (e) => {
  256. e.preventDefault();
  257. const savedCart =
  258. GM_getValue("btnv6_blue_hoverfade btn_small") ?? "";
  259. const cartItems = savedCart.split("\n");
  260. const regFull = new RegExp(/((app|a|bundle|b|sub|s)\/(\d+))/);
  261. const regShort = new RegExp(/^(([\s]*|)(\d+))/);
  262. const dataMap = new Set();
  263.  
  264. for (let line of cartItems) {
  265. let match = line.match(regFull) ?? line.match(regShort);
  266. if (match) {
  267. let [_, link, _1, _2] = match;
  268. dataMap.add(link);
  269. }
  270. }
  271.  
  272. const now = new Date().toLocaleString();
  273. cartItems.push(`========【${now}】=========`);
  274.  
  275. const rows = document.querySelectorAll("#search_resultsRows>a");
  276. for (let row of rows) {
  277. if (
  278. row.className.includes("ds_owned") ||
  279. row.className.includes("ds_ignored")
  280. ) {
  281. continue;
  282. }
  283.  
  284. const url = row.href;
  285. const title =
  286. row.querySelector("span.title")?.textContent ?? "null";
  287.  
  288. let match = url.match(regFull);
  289. if (match) {
  290. let [_, link, _1, _2] = match;
  291.  
  292. if (!dataMap.has(link)) {
  293. cartItems.push(`${link} #${title}`);
  294. }
  295. }
  296. }
  297. GM_setValue("fac_cart", cartItems.join("\n"));
  298. const dialog = showAlert(
  299. t("batchExtractDone"),
  300. t("batchDesc"),
  301. true
  302. );
  303. setTimeout(() => {
  304. dialog.Dismiss();
  305. }, 1500);
  306. },
  307. false
  308. );
  309. btn.className = "btnv6_blue_hoverfade btn_small";
  310. btn.innerHTML = `<span>${t("batchExtract")}</span>`;
  311. searchBar.appendChild(btn);
  312. }
  313. }
  314. }, 500);
  315. } else if (
  316. pathname.startsWith("/publisher/") ||
  317. pathname.startsWith("/franchise/") ||
  318. pathname.startsWith("/developer/")
  319. ) {
  320. //发行商主页
  321. let timer = setInterval(() => {
  322. let container = document.getElementById("RecommendationsRows");
  323. if (container != null) {
  324. clearInterval(timer);
  325. for (let ele of container.querySelectorAll("a.recommendation_link")) {
  326. addButton(ele);
  327. }
  328. container.addEventListener("DOMNodeInserted", ({ relatedNode }) => {
  329. if (relatedNode.nodeName === "DIV") {
  330. for (let ele of relatedNode.querySelectorAll(
  331. "a.recommendation_link"
  332. )) {
  333. addButton(ele);
  334. }
  335. }
  336. });
  337. }
  338. }, 500);
  339. } else if (
  340. pathname.startsWith("/app/") ||
  341. pathname.startsWith("/sub/") ||
  342. pathname.startsWith("/bundle/")
  343. ) {
  344. //商店详情页
  345. let timer = setInterval(() => {
  346. let container = document.getElementById("game_area_purchase");
  347. if (container != null) {
  348. clearInterval(timer);
  349. for (let ele of container.querySelectorAll(
  350. "div.game_area_purchase_game"
  351. )) {
  352. addButton2(ele);
  353. }
  354. }
  355. }, 500);
  356. } else if (pathname.startsWith("/wishlist/")) {
  357. //愿望单页
  358. let timer = setInterval(() => {
  359. let container = document.getElementById("wishlist_ctn");
  360. if (container != null) {
  361. clearInterval(timer);
  362.  
  363. for (let ele of container.querySelectorAll("div.wishlist_row")) {
  364. addButton3(ele);
  365. }
  366. container.addEventListener("DOMNodeInserted", ({ relatedNode }) => {
  367. if (relatedNode.nodeName === "DIV") {
  368. for (let ele of relatedNode.querySelectorAll("div.wishlist_row")) {
  369. addButton3(ele);
  370. }
  371. }
  372. });
  373. }
  374. }, 500);
  375. } else if (pathname.startsWith("/cart/")) {
  376. //购物车页
  377. const continer = document.querySelector("div.cart_area_body");
  378.  
  379. function genBr() {
  380. return document.createElement("br");
  381. }
  382. function genBtn(text, title, onclick) {
  383. let btn = document.createElement("button");
  384. btn.textContent = text;
  385. btn.title = title;
  386. btn.className = "btn_medium btnv6_blue_hoverfade fac_cartbtns";
  387. btn.addEventListener("click", onclick);
  388. return btn;
  389. }
  390. function genSpan(text) {
  391. let span = document.createElement("span");
  392. span.textContent = text;
  393. return span;
  394. }
  395. function genTxt(value, placeholder) {
  396. const t = document.createElement("textarea");
  397. t.className = "fac_inputbox";
  398. t.placeholder = placeholder;
  399. t.value = value;
  400. return t;
  401. }
  402. function genChk(name, title, checked = false) {
  403. const l = document.createElement("label");
  404. const i = document.createElement("input");
  405. const s = genSpan(name);
  406. i.textContent = name;
  407. i.title = title;
  408. i.type = "checkbox";
  409. i.className = "fac_checkbox";
  410. i.checked = checked;
  411. l.appendChild(i);
  412. l.appendChild(s);
  413. return [l, i];
  414. }
  415.  
  416. const savedCart = GM_getValue("fac_cart") ?? "";
  417. const placeHolder = [
  418. t("facInputBoxPlaceHolder"),
  419. `1. ${t("storeLink")}: https://store.steampowered.com/app/xxx`,
  420. `2. ${t("steamDBLink")}: https://steamdb.info/app/xxx`,
  421. "3. appID: xxx a/xxx app/xxx",
  422. "4. subID: s/xxx sub/xxx",
  423. "5. bundleID: b/xxx bundle/xxx",
  424. ].join("\n");
  425.  
  426. const inputBox = genTxt(savedCart, placeHolder);
  427.  
  428. function fitInputBox() {
  429. inputBox.style.height =
  430. Math.min(inputBox.value.split("\n").length * 20 + 20, 900).toString() +
  431. "px";
  432. }
  433.  
  434. inputBox.addEventListener("input", fitInputBox);
  435.  
  436. fitInputBox();
  437.  
  438. const originResetBtn = document.querySelector("div.remove_ctn");
  439. if (originResetBtn != null) {
  440. originResetBtn.style.display = "none";
  441. }
  442.  
  443. const [lblDiscount, chkDiscount] = genChk(
  444. t("onlyOnsale"),
  445. t("onlyOnsaleDesc"),
  446. GM_getValue("fac_discount") ?? false
  447. );
  448.  
  449. const btnArea = document.createElement("div");
  450. const btnImport = genBtn(`🔼${t("import")}`, t("importDesc"), async () => {
  451. inputBox.value = await importCart(
  452. inputBox.value,
  453. false,
  454. chkDiscount.checked
  455. );
  456. window.location.reload();
  457. });
  458. const btnImport2 = genBtn(
  459. `🔼${t("importReverse")}`,
  460. t("importDescReverse"),
  461. async () => {
  462. inputBox.value = await importCart(
  463. inputBox.value,
  464. true,
  465. chkDiscount.checked
  466. );
  467. window.location.reload();
  468. }
  469. );
  470. const histryPage = pathname.search("history") !== -1;
  471. if (histryPage) {
  472. btnImport.disabled = true;
  473. btnImport.title = t("importDesc2");
  474. btnImport2.disabled = true;
  475. btnImport2.title = t("importDesc2");
  476. }
  477.  
  478. const [lblDiscount2, chkDiscount2] = genChk(
  479. t("onlyOnsale"),
  480. t("onlyOnsaleDesc2"),
  481. GM_getValue("fac_discount2") ?? false
  482. );
  483.  
  484. const btnExport = genBtn(`🔽${t("export")}`, t("exportDesc"), () => {
  485. let currentValue = inputBox.value.trim();
  486. const now = new Date().toLocaleString();
  487. if (currentValue !== "") {
  488. ShowConfirmDialog(
  489. "",
  490. t("exportConfirm"),
  491. t("exportConfirmReplace"),
  492. t("exportConfirmAppend")
  493. )
  494. .done(() => {
  495. inputBox.value =
  496. `========【${now}】=========\n` +
  497. exportCart(chkDiscount2.checked);
  498. fitInputBox();
  499. })
  500. .fail((bool) => {
  501. if (bool) {
  502. inputBox.value =
  503. currentValue +
  504. `\n========【${now}】=========\n` +
  505. exportCart(chkDiscount2.checked);
  506. fitInputBox();
  507. }
  508. });
  509. } else {
  510. inputBox.value =
  511. `========【${now}】=========\n` + exportCart(chkDiscount2.checked);
  512. fitInputBox();
  513. }
  514. });
  515.  
  516. const btnCopy = genBtn(`📋${t("copy")}`, t("copyDesc"), () => {
  517. GM_setClipboard(inputBox.value, "text");
  518. showAlert(t("tips"), t("copyDone"), true);
  519. });
  520. const btnClear = genBtn(`🗑️${t("reset")}`, t("resetDesc"), () => {
  521. ShowConfirmDialog("", t("resetConfirm"), t("ok"), t("no")).done(() => {
  522. inputBox.value = "";
  523. GM_setValue("fac_cart", "");
  524. fitInputBox();
  525. });
  526. });
  527. const btnReload = genBtn(`🔃${t("reload")}`, t("reloadDesc"), () => {
  528. ShowConfirmDialog("", t("reloadConfirm"), t("ok"), t("no")).done(() => {
  529. const s = GM_getValue("fac_cart") ?? "";
  530. inputBox.value = s;
  531. fitInputBox();
  532. });
  533. });
  534. const btnHistory = genBtn(`📜${t("history")}`, t("historyDesc"), () => {
  535. window.location.href =
  536. "https://help.steampowered.com/zh-cn/accountdata/ShoppingCartHistory";
  537. });
  538. const btnBack = genBtn(`↩️${t("goBack")}`, t("goBackDesc"), () => {
  539. window.location.href = "https://store.steampowered.com/cart/";
  540. });
  541. const btnForget = genBtn(`⚠️${t("clear")}`, t("clearDesc"), () => {
  542. ShowConfirmDialog("", t("clearConfirm"), t("ok"), t("no")).done(() => {
  543. ForgetCart();
  544. });
  545. });
  546. const btnHelp = genBtn(`🔣${t("help")}`, t("helpDesc"), () => {
  547. const {
  548. script: { version },
  549. } = GM_info;
  550. showAlert(
  551. `${t("helpTitle")} ${version}`,
  552. [
  553. `<p>【🔼${t("import")}】${t("importDesc")}</p>`,
  554. `<p>【🔼${t("importReverse")}】${t("importDescReverse")}</p>`,
  555. `<p>【✅${t("onlyOnsale")}】${t("onlyOnsaleDesc")}</p>`,
  556. `<p>【🔽${t("export")}】${t("exportDesc")}</p>`,
  557. `<p>【✅${t("onlyOnsale")}】${t("onlyOnsaleDesc2")}</p>`,
  558. `<p>【📋${t("copy")}】${t("copyDesc")}</p>`,
  559. `<p>【🗑️${t("reset")}】${t("resetDesc")}。</p>`,
  560. `<p>【📜${t("history")}】${t("historyDesc")}</p>`,
  561. `<p>【↩️${t("goBack")}】${t("goBackDesc")}</p>`,
  562. `<p>【⚠️${t("clear")}】${t("clearDesc")}</p>`,
  563. `<p>【🔣${t("help")}】${t("helpDesc")}</p>`,
  564. `<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>`,
  565. ].join("<br>"),
  566. true
  567. );
  568. });
  569.  
  570. btnArea.appendChild(btnImport);
  571. btnArea.appendChild(btnImport2);
  572. btnArea.appendChild(lblDiscount);
  573. btnArea.appendChild(genSpan(" | "));
  574. btnArea.appendChild(btnExport);
  575. btnArea.appendChild(lblDiscount2);
  576. btnArea.appendChild(genSpan(" | "));
  577. btnArea.appendChild(btnHelp);
  578.  
  579. continer.appendChild(btnArea);
  580. btnArea.appendChild(genBr());
  581. btnArea.appendChild(genBr());
  582. continer.appendChild(inputBox);
  583.  
  584. const btnArea2 = document.querySelector("div.continue_shopping_ctn");
  585. btnArea2.innerHTML = "";
  586.  
  587. btnArea2.appendChild(btnCopy);
  588. btnArea2.appendChild(btnClear);
  589. btnArea2.appendChild(btnReload);
  590. btnArea2.appendChild(genSpan(" | "));
  591. btnArea2.appendChild(histryPage ? btnBack : btnHistory);
  592. btnArea2.appendChild(genSpan(" | "));
  593. btnArea2.appendChild(btnForget);
  594.  
  595. window.addEventListener("beforeunload", () => {
  596. GM_setValue("fac_cart", inputBox.value);
  597. GM_setValue("fac_discount", chkDiscount.checked);
  598. GM_setValue("fac_discount2", chkDiscount2.checked);
  599. });
  600. }
  601.  
  602. //始终在右上角显示购物车按钮
  603. const cart_btn = document.getElementById("store_header_cart_btn");
  604. if (cart_btn !== null) {
  605. cart_btn.style.display = "";
  606. }
  607.  
  608. //导入购物车
  609. function importCart(text, reverse = false, onlyOnSale = false) {
  610. return new Promise(async (resolve, reject) => {
  611. const regFull = new RegExp(/(app|a|bundle|b|sub|s)\/(\d+)/);
  612. const regShort = new RegExp(/^([\s]*|)(\d+)/);
  613. let lines = [];
  614.  
  615. let dialog = showAlert(
  616. t("importingTitle"),
  617. `<textarea id="fac_diag" class="fac_diag">${t("operation")}</textarea>`,
  618. true
  619. );
  620.  
  621. let timer = setInterval(async () => {
  622. let txt = document.getElementById("fac_diag");
  623. if (txt !== null) {
  624. clearInterval(timer);
  625.  
  626. const txts = reverse ? text.split("\n").reverse() : text.split("\n");
  627.  
  628. for (let line of txts) {
  629. if (line.trim() === "") {
  630. continue;
  631. }
  632. let match = line.match(regFull) ?? line.match(regShort);
  633. if (!match) {
  634. if (line.search("=====") === -1) {
  635. let tmp = line.split("#")[0];
  636. lines.push(`${tmp} #${t("formatError")}`);
  637. } else {
  638. lines.push(line);
  639. }
  640. continue;
  641. }
  642. let [_, type, subID] = match;
  643. switch (type.toLowerCase()) {
  644. case "":
  645. case "a":
  646. case "app":
  647. type = "app";
  648. break;
  649. case "s":
  650. case "sub":
  651. type = "sub";
  652. break;
  653. case "b":
  654. case "bundle":
  655. type = "bundle";
  656. break;
  657. default:
  658. let tmp = line.split("#")[0];
  659. lines.push(`${tmp} #${t("formatError")}`);
  660. continue;
  661. }
  662.  
  663. if (type === "sub" || type === "bundle") {
  664. let [succ, msg] = await addCart(type, subID, "");
  665. lines.push(`${type}/${subID} #${msg}`);
  666. } else {
  667. try {
  668. let subInfos = await getGameSubs(subID);
  669. let [sID, subName, discount, price] = subInfos[0];
  670. if (onlyOnSale && discount.length === 0) {
  671. lines.push(
  672. `${type}/${subID} #${subName} - ${discount}${price} ${t(
  673. "notOnSale"
  674. )}`
  675. );
  676. } else {
  677. let [succ, msg] = await addCart("sub", sID, subID);
  678. lines.push(
  679. `${type}/${subID} #${subName} - ${discount}${price} ${msg}`
  680. );
  681. }
  682. } catch (e) {
  683. lines.push(`${type}/${subID} #${t("noSubFound")}`);
  684. }
  685. }
  686. txt.value = reverse ? lines.reverse().join("\n") : lines.join("\n");
  687. txt.scrollTop = txt.scrollHeight;
  688. }
  689. }
  690.  
  691. dialog.Dismiss();
  692. resolve(lines.join("\n"));
  693. }, 200);
  694. });
  695. }
  696. //导出购物车
  697. function exportCart(onlyOnsale = false) {
  698. const regMatch = new RegExp(/(app|sub|bundle)_(\d+)/i);
  699. let data = [];
  700. for (let item of document.querySelectorAll(
  701. "div.cart_item_list>div.cart_row"
  702. )) {
  703. const priceEle = item.querySelector("div.cart_item_price");
  704. const discount = priceEle?.classList.contains("with_discount")
  705. ? "🔖 "
  706. : "";
  707. const price = priceEle.querySelector("div.price")?.textContent ?? "Null";
  708.  
  709. let itemKey = item.getAttribute("data-ds-itemkey");
  710. let name = item.querySelector(".cart_item_desc>a").innerText.trim();
  711. let match = itemKey.toLowerCase().match(regMatch);
  712. if (match) {
  713. let [_, type, id] = match;
  714.  
  715. if (onlyOnsale && discount.length === 0) {
  716. continue;
  717. }
  718.  
  719. data.push(`${type}/${id} #${name} ${discount}💳${price}`);
  720. }
  721. }
  722. return data.join("\n");
  723. }
  724. //添加按钮
  725. function addButton(element) {
  726. if (element.getAttribute("added") !== null) {
  727. return;
  728. }
  729. element.setAttribute("added", "");
  730.  
  731. if (element.href === undefined) {
  732. return;
  733. }
  734.  
  735. let appID = (element.href.match(/\/app\/(\d+)/) ?? [null, null])[1];
  736. if (appID === null) {
  737. return;
  738. }
  739.  
  740. let btn = document.createElement("button");
  741. btn.addEventListener(
  742. "click",
  743. (e) => {
  744. chooseSubs(appID);
  745. e.preventDefault();
  746. },
  747. false
  748. );
  749. btn.className = "fac_listbtns";
  750. btn.textContent = "🛒";
  751. element.appendChild(btn);
  752. }
  753. //添加按钮
  754. function addButton2(element) {
  755. if (element.getAttribute("added") !== null) {
  756. return;
  757. }
  758. element.setAttribute("added", "");
  759. let type, subID;
  760.  
  761. let parentElement = element.parentElement;
  762.  
  763. if (parentElement.hasAttribute("data-ds-itemkey")) {
  764. let itemKey = parentElement.getAttribute("data-ds-itemkey");
  765. let match = itemKey.toLowerCase().match(/(app|sub|bundle)_(\d+)/);
  766. if (match) {
  767. [, type, subID] = match;
  768. }
  769. } else if (
  770. parentElement.hasAttribute("data-ds-bundleid") ||
  771. parentElement.hasAttribute("data-ds-subid")
  772. ) {
  773. subID =
  774. parentElement.getAttribute("data-ds-subid") ??
  775. parentElement.getAttribute("data-ds-bundleid");
  776. type = parentElement.hasAttribute("data-ds-subid") ? "sub" : "bundle";
  777. } else {
  778. let match = element.id.match(/cart_(\d+)/);
  779. if (match) {
  780. type = "sub";
  781. [, subID] = match;
  782. }
  783. }
  784.  
  785. if (type === undefined || subID === undefined) {
  786. console.warn(t("addCartErrorSubNotFount"));
  787. return;
  788. }
  789.  
  790. const btnBar = element.querySelector("div.game_purchase_action");
  791. const firstItem = element.querySelector("div.game_purchase_action_bg");
  792. if (
  793. btnBar === null ||
  794. firstItem == null ||
  795. type === undefined ||
  796. subID === undefined
  797. ) {
  798. return;
  799. }
  800. let appID = (window.location.pathname.match(/\/(app)\/(\d+)/) ?? [
  801. null,
  802. null,
  803. null,
  804. ])[2];
  805. let btn = document.createElement("button");
  806. btn.addEventListener(
  807. "click",
  808. async () => {
  809. let dialog = showAlert(
  810. t("operation"),
  811. `<p>${t("addCartTips")}</p>`,
  812. true
  813. );
  814. let [succ, msg] = await addCart(type, subID, appID);
  815. let done = showAlert(t("operationDone"), `<p>${msg}</p>`, succ);
  816. setTimeout(() => {
  817. done.Dismiss();
  818. }, 1200);
  819. dialog.Dismiss();
  820. if (succ) {
  821. let acBtn = btnBar.querySelector("div[class='btn_addtocart']>a");
  822. if (acBtn) {
  823. acBtn.href = "https://store.steampowered.com/cart/";
  824. acBtn.innerHTML = `\n\t\n<span>${t("inCart")}</span>\n\t\n`;
  825. }
  826. }
  827. },
  828. false
  829. );
  830. btn.className = "fac_listbtns";
  831. btn.textContent = "🛒";
  832. btnBar.insertBefore(btn, firstItem);
  833. }
  834. //添加按钮
  835. function addButton3(element) {
  836. if (element.getAttribute("added") !== null) {
  837. return;
  838. }
  839. element.setAttribute("added", "");
  840.  
  841. let appID = element.getAttribute("data-app-id");
  842. if (appID === null) {
  843. return;
  844. }
  845.  
  846. let btn = document.createElement("button");
  847. btn.addEventListener(
  848. "click",
  849. (e) => {
  850. chooseSubs(appID);
  851. e.preventDefault();
  852. },
  853. false
  854. );
  855. btn.className = "fac_listbtns";
  856. btn.textContent = "🛒";
  857. element.appendChild(btn);
  858. }
  859. //选择SUB
  860. async function chooseSubs(appID) {
  861. let dialog = showAlert(t("operation"), `<p>${t("fetchingSubs")}</p>`, true);
  862. getGameSubs(appID)
  863. .then(async (subInfos) => {
  864. if (subInfos.length === 0) {
  865. showAlert(
  866. t("addCartError"),
  867. `<p>${t("noSubFound")}, ${t("noSubDesc")}.</p>`,
  868. false
  869. );
  870. dialog.Dismiss();
  871. return;
  872. } else {
  873. if (subInfos.length === 1) {
  874. let [subID, subName, discount, price] = subInfos[0];
  875. await addCart("sub", subID, appID);
  876. let done = showAlert(
  877. t("addCartSuccess"),
  878. `<p>${subName} - ${discount}${price}</p>`,
  879. true
  880. );
  881. setTimeout(() => {
  882. done.Dismiss();
  883. }, 1200);
  884. dialog.Dismiss();
  885. } else {
  886. let dialog2 = showAlert(
  887. t("chooseSub"),
  888. "<div id=fac_choose></div>",
  889. true
  890. );
  891. dialog.Dismiss();
  892. await new Promise((resolve) => {
  893. let timer = setInterval(() => {
  894. if (document.getElementById("fac_choose") !== null) {
  895. clearInterval(timer);
  896. resolve();
  897. }
  898. }, 200);
  899. });
  900. let divContiner = document.getElementById("fac_choose");
  901. for (let [subID, subName, discount, price] of subInfos) {
  902. let btn = document.createElement("button");
  903. btn.addEventListener("click", async () => {
  904. let dialog = showAlert(
  905. t("operation"),
  906. `<p>${t("add")} ${subName} - ${discount}${price} ${t(
  907. "toCart"
  908. )}</p>`,
  909. true
  910. );
  911. dialog2.Dismiss();
  912. let [succ, msg] = await addCart("sub", subID, appID);
  913. let done = showAlert(
  914. msg,
  915. `<p>${subName} - ${discount}${price}</p>`,
  916. succ
  917. );
  918. setTimeout(() => {
  919. done.Dismiss();
  920. }, 1200);
  921. dialog.Dismiss();
  922. });
  923. btn.textContent = `🛒${t("addCart")}`;
  924. btn.className = "fac_choose";
  925. let p = document.createElement("p");
  926. p.textContent = `${subName} - ${discount}${price}`;
  927. p.appendChild(btn);
  928. divContiner.appendChild(p);
  929. }
  930. }
  931. }
  932. })
  933. .catch((err) => {
  934. let done = showAlert(t("networkError"), `<p>${err}</p>`, false);
  935. setTimeout(() => {
  936. done.Dismiss();
  937. }, 2000);
  938. dialog.Dismiss();
  939. });
  940. }
  941. //读取sub信息
  942. function getGameSubs(appID) {
  943. return new Promise((resolve, reject) => {
  944. const regPure = new RegExp(/ - [^-]*$/, "");
  945. const regSymbol = new RegExp(/[>-] ([^>-]+) [\d.]+$/, "");
  946. const lang = document.cookie.replace(
  947. /(?:(?:^|.*;\s*)Steam_Language\s*\=\s*([^;]*).*$)|^.*$/,
  948. "$1"
  949. );
  950. fetch(
  951. `https://store.steampowered.com/api/appdetails?appids=${appID}&lang=${lang}`,
  952. {
  953. method: "GET",
  954. credentials: "include",
  955. }
  956. )
  957. .then(async (response) => {
  958. if (response.ok) {
  959. let data = await response.json();
  960. let result = data[appID];
  961. if (result.success !== true) {
  962. reject(t("unrecognizedResult"));
  963. }
  964. let subInfos = [];
  965. for (let pkg of result.data.package_groups) {
  966. for (let sub of pkg.subs) {
  967. const {
  968. packageid,
  969. option_text,
  970. percent_savings_text,
  971. price_in_cents_with_discount,
  972. } = sub;
  973. if (price_in_cents_with_discount > 0) {
  974. //排除免费SUB
  975. const symbol = option_text.match(regSymbol)?.pop();
  976. const subName = option_text.replace(regPure, "");
  977. const price =
  978. "💳" + price_in_cents_with_discount / 100 + " " + symbol;
  979. const discount =
  980. percent_savings_text !== " "
  981. ? "🔖" + percent_savings_text + " "
  982. : "";
  983. subInfos.push([packageid, subName, discount, price]);
  984. }
  985. }
  986. }
  987. console.info(subInfos);
  988. resolve(subInfos);
  989. } else {
  990. reject(t("networkRequestError"));
  991. }
  992. })
  993. .catch((err) => {
  994. reject(err);
  995. });
  996. });
  997. }
  998. //添加购物车,只支持subID和bundleID
  999. function addCart(type = "sub", subID, appID = null) {
  1000. window.localStorage["fac_subid"] = subID;
  1001. return new Promise((resolve, reject) => {
  1002. let data = {
  1003. action: "add_to_cart",
  1004. originating_snr: "1_store-navigation__",
  1005. sessionid: document.cookie.replace(
  1006. /(?:(?:^|.*;\s*)sessionid\s*\=\s*([^;]*).*$)|^.*$/,
  1007. "$1"
  1008. ),
  1009. snr: "1_5_9__403",
  1010. };
  1011. data[`${type}id`] = String(subID);
  1012. let s = "";
  1013. for (let k in data) {
  1014. s += `${k}=${encodeURIComponent(data[k])}&`;
  1015. }
  1016. fetch("https://store.steampowered.com/cart/", {
  1017. method: "POST",
  1018. credentials: "include",
  1019. body: s,
  1020. headers: {
  1021. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  1022. },
  1023. })
  1024. .then(async (response) => {
  1025. if (response.ok) {
  1026. let data = await response.text();
  1027. if (appID !== null) {
  1028. const regIfSucc = new RegExp("app/" + appID);
  1029. if (data.search(regIfSucc) !== -1) {
  1030. resolve([true, t("addCartSuccess")]);
  1031. } else {
  1032. resolve([false, t("addCartError")]);
  1033. }
  1034. } else {
  1035. resolve([true, t("addCartSuccess")]);
  1036. }
  1037. } else {
  1038. resolve([false, t("networkRequestError")]);
  1039. }
  1040. })
  1041. .catch((err) => {
  1042. console.error(err);
  1043. resolve([false, `${t("unknownError")}: ${err}`]);
  1044. });
  1045. });
  1046. }
  1047. //显示提示
  1048. function showAlert(title, text, succ = true) {
  1049. return ShowAlertDialog(`${succ ? "✅" : "❌"}${title}`, text);
  1050. }
  1051. })();
  1052.  
  1053. GM_addStyle(`
  1054. button.fac_listbtns {
  1055. display: none;
  1056. position: relative;
  1057. z-index: 100;
  1058. padding: 1px;
  1059. }
  1060. a.search_result_row > button.fac_listbtns {
  1061. top: -25px;
  1062. left: 300px;
  1063. }
  1064. a.tab_item > button.fac_listbtns {
  1065. top: -40px;
  1066. left: 330px;
  1067. }
  1068. a.recommendation_link > button.fac_listbtns {
  1069. bottom: 10px;
  1070. right: 10px;
  1071. position: absolute;
  1072. }
  1073. div.wishlist_row > button.fac_listbtns {
  1074. top: 35%;
  1075. right: 30%;
  1076. position: absolute;
  1077. }
  1078. div.game_purchase_action > button.fac_listbtns {
  1079. right: 8px;
  1080. bottom: 8px;
  1081. }
  1082. button.fac_cartbtns {
  1083. padding: 5px 10px;
  1084. }
  1085. button.fac_cartbtns:not(:last-child) {
  1086. margin-right: 5px;
  1087. }
  1088. button.fac_cartbtns:not(:first-child) {
  1089. margin-left: 5px;
  1090. }
  1091. a.tab_item:hover button.fac_listbtns,
  1092. a.search_result_row:hover button.fac_listbtns,
  1093. div.recommendation:hover button.fac_listbtns,
  1094. div.wishlist_row:hover button.fac_listbtns {
  1095. display: block;
  1096. }
  1097. div.game_purchase_action:hover > button.fac_listbtns {
  1098. display: inline;
  1099. }
  1100. button.fac_choose {
  1101. padding: 1px;
  1102. margin: 2px 5px;
  1103. }
  1104. textarea.fac_inputbox {
  1105. height: 130px;
  1106. resize: vertical;
  1107. font-size: 10px;
  1108. min-height: 130px;
  1109. }
  1110. textarea.fac_diag {
  1111. height: 150px;
  1112. width: 600px;
  1113. resize: vertical;
  1114. font-size: 10px;
  1115. margin-bottom: 5px;
  1116. padding: 5px;
  1117. background-color: rgba(0, 0, 0, 0.4);
  1118. color: #fff;
  1119. border: 1 px solid #000;
  1120. border-radius: 3 px;
  1121. box-shadow: 1px 1px 0px #45556c;
  1122. }
  1123. `);

QingJ © 2025

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