Steam快速添加购物车

超级方便的添加购物车体验,不用跳转商店页。

目前为 2021-10-11 提交的版本。查看 最新版本

  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 2.14
  8. // @description 超级方便的添加购物车体验,不用跳转商店页。
  9. // @description:zh-CN 超级方便的添加购物车体验,不用跳转商店页。
  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_info
  19. // ==/UserScript==
  20.  
  21. (async () => {
  22. 'use strict';
  23. //初始化
  24. let pathname = window.location.pathname;
  25. if (pathname === '/search/' || pathname === '/' || pathname.startsWith('/tags/')) { //搜索页,主页,标签页
  26. let t = setInterval(() => {
  27. let containers = document.querySelectorAll([
  28. '#search_resultsRows',
  29. '#tab_newreleases_content',
  30. '#tab_topsellers_content',
  31. '#tab_upcoming_content',
  32. '#tab_specials_content',
  33. '#NewReleasesRows',
  34. '#TopSellersRows',
  35. '#ConcurrentUsersRows',
  36. '#TopRatedRows',
  37. '#ComingSoonRows'
  38. ].join(','));
  39. if (containers.length > 0) {
  40. for (let container of containers) {
  41. clearInterval(t);
  42. for (let ele of container.children) {
  43. addButton(ele);
  44. }
  45. container.addEventListener('DOMNodeInserted', ({ relatedNode }) => {
  46. if (relatedNode.parentElement === container) {
  47. addButton(relatedNode);
  48. }
  49. });
  50. }
  51. }
  52. }, 500);
  53. } else if (pathname.startsWith('/publisher/') || pathname.startsWith('/franchise/')) { //发行商主页
  54. let t = setInterval(() => {
  55. let container = document.getElementById('RecommendationsRows');
  56. if (container != null) {
  57. clearInterval(t);
  58. for (let ele of container.querySelectorAll('a.recommendation_link')) {
  59. addButton(ele);
  60. }
  61. container.addEventListener('DOMNodeInserted', ({ relatedNode }) => {
  62. if (relatedNode.nodeName === 'DIV') {
  63. console.log(relatedNode);
  64. for (let ele of relatedNode.querySelectorAll('a.recommendation_link')) {
  65. addButton(ele);
  66. }
  67. }
  68. });
  69. }
  70. }, 500);
  71. } else if (pathname.startsWith('/app/') || pathname.startsWith('/sub/') || pathname.startsWith('/bundle/')) { //商店详情页
  72. let t = setInterval(() => {
  73. let container = document.getElementById('game_area_purchase');
  74. if (container != null) {
  75. clearInterval(t);
  76. for (let ele of container.querySelectorAll('div.game_area_purchase_game')) {
  77. addButton2(ele);
  78. }
  79. }
  80. }, 500);
  81. } else if (pathname.startsWith('/wishlist/')) { //愿望单页
  82. let t = setInterval(() => {
  83. let container = document.getElementById('wishlist_ctn');
  84. if (container != null) {
  85. clearInterval(t);
  86.  
  87. for (let ele of container.querySelectorAll('div.wishlist_row')) {
  88. addButton3(ele);
  89. }
  90. container.addEventListener('DOMNodeInserted', ({ relatedNode }) => {
  91. if (relatedNode.nodeName === 'DIV') {
  92. console.log(relatedNode);
  93. for (let ele of relatedNode.querySelectorAll('div.wishlist_row')) {
  94. addButton3(ele);
  95. }
  96. }
  97. });
  98. }
  99. }, 500);
  100. } else if (pathname === '/cart/') { //购物车页
  101. let continer = document.querySelector('div.cart_area_body');
  102.  
  103. let genBr = () => { return document.createElement('br'); };
  104. let genBtn = (text, title, onclick) => {
  105. let btn = document.createElement('button');
  106. btn.textContent = text;
  107. btn.title = title;
  108. btn.className = 'btn_medium btnv6_blue_hoverfade fac_cartbtns';
  109. btn.addEventListener('click', onclick);
  110. return btn;
  111. };
  112. let genSpan = (text) => {
  113. let span = document.createElement('span');
  114. span.textContent = text;
  115. return span;
  116. };
  117. let inputBox = document.createElement('textarea');
  118. inputBox.value = GM_getValue('fac_cart') ?? '';
  119. inputBox.className = 'fac_inputbox';
  120. inputBox.placeholder = ['一行一条, 自动忽略【#】后面的内容, 支持的格式如下: (自动保存)',
  121. '1. 商店链接: https://store.steampowered.com/app/xxx',
  122. '2. DB链接: https://steamdb.info/app/xxx',
  123. '3. appID: xxx a/xxx app/xxx',
  124. '4. subID: s/xxx sub/xxx',
  125. '5. bundleID: b/xxx bundle/xxx'
  126. ].join('\n');
  127.  
  128. let btnArea = document.createElement('div');
  129. let btnImport = genBtn('🔼批量导入', '从文本框批量添加购物车', async () => {
  130. inputBox.value = await importCart(inputBox.value);
  131. window.location.reload();
  132. });
  133. let btnExport = genBtn('🔽导出', '将购物车内容导出至文本框', () => { inputBox.value = exportCart(); });
  134. let btnCopy = genBtn('📋复制', '复制文本框中的内容', () => {
  135. GM_setClipboard(inputBox.value, { type: 'text', mimetype: 'text/plain' });
  136. showAlert('提示', '复制到剪贴板成功', true);
  137. });
  138. let btnClear = genBtn('🗑️清除', '清除文本框和已保存的数据', () => {
  139. inputBox.value = '';
  140. GM_setValue('fac_cart', '');
  141. showAlert('提示', '文本框内容和保存的数据已清除', true);
  142. });
  143. let btnForget = genBtn('⚠️清空', '清空购物车', () => {
  144. ShowConfirmDialog('', '您确定要移除所有您购物车中的物品吗?', '是', '否')
  145. .done(() => {
  146. ForgetCart();
  147. window.location.reload();
  148. });
  149. });
  150. let btnHelp = genBtn('🔣帮助', '显示帮助', () => {
  151. const { script: { version } } = GM_info;
  152. showAlert(`帮助 插件版本 ${version}`, [
  153. '<p>【🔼批量导入】从文本框批量添加购物车。</p>',
  154. '<p>【🔽导出】将购物车内容导出至文本框。</p>',
  155. '<p>【📋复制】复制文本框中的内容(废话)。</p>',
  156. '<p>【🗑️清除】清除文本框和已保存的数据。</p>',
  157. '<p>【⚠️清空】清空购物车。</p>',
  158. '<p>【🔣帮助】显示没什么卵用的帮助。</p>',
  159. '<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_>Chr_</a>】</p>'
  160. ].join('<br>'), true)
  161. });
  162.  
  163. btnArea.appendChild(btnImport);
  164. btnArea.appendChild(btnExport);
  165. btnArea.appendChild(genSpan(' | '));
  166. btnArea.appendChild(btnCopy);
  167. btnArea.appendChild(btnClear);
  168. btnArea.appendChild(genSpan(' | '));
  169. btnArea.appendChild(btnForget);
  170. btnArea.appendChild(genSpan(' | '));
  171. btnArea.appendChild(btnHelp);
  172.  
  173. continer.appendChild(btnArea);
  174. btnArea.appendChild(genBr());
  175. btnArea.appendChild(genBr());
  176. continer.appendChild(inputBox);
  177.  
  178. window.addEventListener('beforeunload', () => { GM_setValue('fac_cart', inputBox.value); })
  179. }
  180. //导入购物车
  181. function importCart(text) {
  182. return new Promise(async (resolve, reject) => {
  183. const regFull = new RegExp(/(app|a|bundle|b|sub|s)\/(\d+)/);
  184. const regShort = new RegExp(/()(\d+)/);
  185. let lines = [];
  186. let dialog = showAlert('操作中……', '正在导入购物车...', true);
  187. for (let line of text.split('\n')) {
  188. if (line.trim() === '') {
  189. continue;
  190. }
  191. let match = line.match(regFull) ?? line.match(regShort);
  192. if (!match) {
  193. let tmp = line.split('#')[0];
  194. lines.push(`${tmp} #格式有误`);
  195. continue;
  196. }
  197. let [_, type, subID] = match;
  198. switch (type.toLowerCase()) {
  199. case '':
  200. case 'a':
  201. case 'app':
  202. type = 'app';
  203. break;
  204. case 's':
  205. case 'sub':
  206. type = 'sub';
  207. break;
  208. case 'b':
  209. case 'bundle':
  210. type = 'bundle';
  211. break;
  212. default:
  213. let tmp = line.split('#')[0];
  214. lines.push(`${tmp} #格式有误`);
  215. continue;
  216. }
  217.  
  218. if (type === 'sub' || type === 'bundle') {
  219. let [succ, msg] = await addCart(type, subID, '');
  220. lines.push(`${type}/${subID} #${msg}`);
  221. } else {
  222. try {
  223. let subInfos = await getGameSubs(subID);
  224. let [sID, subName, discount, price] = subInfos[0];
  225. let [succ, msg] = await addCart('sub', sID, subID);
  226. lines.push(`${type}/${subID} #${subName} - ${price} ${msg}`);
  227. } catch (e) {
  228. lines.push(`${type}/${subID} #未找到可用SUB`);
  229. }
  230. }
  231. let d = showAlert('操作中……', `<p>${lines.join('</p><p>')}</p>`, true);
  232. setTimeout(() => { d.Dismiss(); }, 1200);
  233. }
  234. dialog.Dismiss();
  235. resolve(lines.join('\n'));
  236. });
  237. }
  238. //导出购物车
  239. function exportCart() {
  240. let data = [];
  241. let regMatch = new RegExp(/(app|sub|bundle)_(\d+)/);
  242. for (let item of document.querySelectorAll('div.cart_item_list>div.cart_row ')) {
  243. let itemKey = item.getAttribute('data-ds-itemkey');
  244. let name = item.querySelector('.cart_item_desc').textContent.trim();
  245. let match = itemKey.toLowerCase().match(regMatch);
  246. if (match) {
  247. let [_, type, id] = match;
  248. data.push(`${type}/${id} #${name}`);
  249. }
  250. }
  251. return data.join('\n');
  252. }
  253. //添加按钮
  254. function addButton(element) {
  255. if (element.getAttribute('added') !== null) { return; }
  256. element.setAttribute('added', '');
  257.  
  258. if (element.href === undefined) { return; }
  259.  
  260. let appID = (element.href.match(/\/app\/(\d+)/) ?? [null, null])[1];
  261. if (appID === null) { return; }
  262.  
  263. let btn = document.createElement('button');
  264. btn.addEventListener('click', (e) => {
  265. chooseSubs(appID);
  266. e.preventDefault();
  267. }, false);
  268. btn.className = 'fac_listbtns';
  269. btn.textContent = '🛒';
  270. element.appendChild(btn);
  271. }
  272. //添加按钮
  273. function addButton2(element) {
  274. if (element.getAttribute('added') !== null) { return; }
  275. element.setAttribute('added', '');
  276. let type, subID;
  277.  
  278. let parentElement = element.parentElement;
  279.  
  280. if (parentElement.hasAttribute('data-ds-itemkey')) {
  281. let itemKey = parentElement.getAttribute('data-ds-itemkey');
  282. let match = itemKey.toLowerCase().match(/(app|sub|bundle)_(\d+)/);
  283. if (match) { [, type, subID] = match; }
  284. } else if (parentElement.hasAttribute('data-ds-bundleid') || parentElement.hasAttribute('data-ds-subid')) {
  285. subID = parentElement.getAttribute('data-ds-subid') ?? parentElement.getAttribute('data-ds-bundleid');
  286. type = parentElement.hasAttribute('data-ds-subid') ? 'sub' : 'bundle';
  287. } else {
  288. let match = element.id.match(/cart_(\d+)/);
  289. if (match) {
  290. type = 'sub';
  291. [, subID] = match;
  292. }
  293. }
  294.  
  295. if (type === undefined || subID === undefined) { console.log('未识别到subID'); return; }
  296.  
  297. const btnBar = element.querySelector('div.game_purchase_action');
  298. const firstItem = element.querySelector('div.game_purchase_action_bg');
  299. if (btnBar === null || firstItem == null || type === undefined || subID === undefined) { return; }
  300. let appID = (window.location.pathname.match(/\/(app)\/(\d+)/) ?? [null, null, null])[2];
  301. let btn = document.createElement('button');
  302. btn.addEventListener('click', async () => {
  303. let dialog = showAlert('操作中……', '<p>添加到购物车……</p>', true);
  304. let [succ, msg] = await addCart(type, subID, appID);
  305. let done = showAlert('操作完成', `<p>${msg}</p>`, succ);
  306. setTimeout(() => { done.Dismiss(); }, 1200);
  307. dialog.Dismiss();
  308. if (succ) {
  309. let acBtn = btnBar.querySelector('div[class="btn_addtocart"]>a');
  310. if (acBtn) {
  311. acBtn.href = 'https://store.steampowered.com/cart/';
  312. acBtn.innerHTML = '\n\t\n<span>在购物车中</span>\n\t\n';
  313. }
  314. }
  315. }, false);
  316. btn.className = 'fac_listbtns';
  317. btn.textContent = '🛒';
  318. btnBar.insertBefore(btn, firstItem);
  319. }
  320. //添加按钮
  321. function addButton3(element) {
  322. if (element.getAttribute('added') !== null) { return; }
  323. element.setAttribute('added', '');
  324.  
  325. let appID = element.getAttribute('data-app-id');
  326. if (appID === null) { return; }
  327.  
  328. let btn = document.createElement('button');
  329. btn.addEventListener('click', (e) => {
  330. chooseSubs(appID);
  331. e.preventDefault();
  332. }, false);
  333. btn.className = 'fac_listbtns';
  334. btn.textContent = '🛒';
  335. element.appendChild(btn);
  336. }
  337. //选择SUB
  338. async function chooseSubs(appID) {
  339. let dialog = showAlert('操作中……', '<p>读取可用SUB</p>', true);
  340. getGameSubs(appID)
  341. .then(async (subInfos) => {
  342. if (subInfos.length === 0) {
  343. showAlert('添加购物车失败', '<p>未找到可用SUB, 可能尚未发行或者是免费游戏.</p>', false);
  344. dialog.Dismiss();
  345. return;
  346. } else {
  347. console.log(subInfos);
  348. if (subInfos.length === 1) {
  349. let [subID, subName, discount, price] = subInfos[0];
  350. await addCart('sub', subID, appID);
  351. let done = showAlert('添加购物车成功', `<p>${subName} - ${price}</p>`, true);
  352. setTimeout(() => { done.Dismiss(); }, 1200);
  353. dialog.Dismiss();
  354. } else {
  355. let dialog2 = showAlert('请选择SUB', '<div id=fac_choose></div>', true);
  356. dialog.Dismiss();
  357. await new Promise((resolve) => {
  358. let t = setInterval(() => {
  359. if (document.getElementById('fac_choose') !== null) {
  360. clearInterval(t);
  361. resolve();
  362. }
  363. }, 200);
  364. });
  365. let divContiner = document.getElementById('fac_choose');
  366. for (let [subID, subName, discount, price] of subInfos) {
  367. let btn = document.createElement('button');
  368. btn.addEventListener('click', async () => {
  369. let dialog = showAlert('操作中……', `<p>添加 ${subName} - ${price} 到购物车</p>`, true);
  370. dialog2.Dismiss();
  371. let [succ, msg] = await addCart('sub', subID, appID);
  372. let done = showAlert(msg, `<p>${subName} - ${price}</p>`, succ);
  373. setTimeout(() => { done.Dismiss(); }, 1200);
  374. dialog.Dismiss();
  375. });
  376. btn.textContent = '🛒添加购物车';
  377. btn.className = 'fac_choose';
  378. let p = document.createElement('p');
  379. p.textContent = `${subName} - ${price}`;
  380. p.appendChild(btn);
  381. divContiner.appendChild(p);
  382. }
  383. }
  384. }
  385.  
  386. })
  387. .catch(err => {
  388. let done = showAlert('网络错误', `<p>${err}</p>`, false);
  389. setTimeout(() => { done.Dismiss(); }, 2000);
  390. dialog.Dismiss();
  391. });
  392. }
  393. //读取sub信息
  394. function getGameSubs(appID) {
  395. return new Promise((resolve, reject) => {
  396. const regPure = new RegExp(/ - [^-]*$/, '');
  397. const regSymbol = new RegExp(/[> ] (.+) \d/, '');
  398. const lang = document.cookie.replace(/(?:(?:^|.*;\s*)Steam_Language\s*\=\s*([^;]*).*$)|^.*$/, "$1")
  399. fetch(`https://store.steampowered.com/api/appdetails?appids=${appID}&lang=${lang}`, {
  400. method: 'GET',
  401. credentials: 'include',
  402. })
  403. .then(async response => {
  404. if (response.ok) {
  405. let data = await response.json();
  406. let result = data[appID];
  407. if (result.success !== true) {
  408. reject('返回了未知结果');
  409. }
  410. let subInfos = [];
  411. for (let pkg of result.data.package_groups) {
  412. for (let sub of pkg.subs) {
  413. const { packageid, option_text, percent_savings_text, price_in_cents_with_discount } = sub;
  414. if (price_in_cents_with_discount > 0) { //排除免费SUB
  415. let symbol = option_text.match(regSymbol)?.pop();
  416. let price = price_in_cents_with_discount / 100 + ' ' + symbol;
  417. let subName = option_text.replace(regPure, '');
  418. if (percent_savings_text === ' ') {
  419. subInfos.push([packageid, subName, percent_savings_text, price]);
  420. } else {
  421. subInfos.push([packageid, subName, false, price]);
  422. }
  423. }
  424. }
  425. }
  426. resolve(subInfos);
  427. } else {
  428. reject('网络请求失败');
  429. }
  430. }).catch(err => {
  431. reject(err);
  432. });
  433. });
  434. }
  435. //添加购物车,只支持subID和bundleID
  436. function addCart(type = 'sub', subID, appID = null) {
  437. window.localStorage['fac_subid'] = subID;
  438. return new Promise((resolve, reject) => {
  439. let data = {
  440. action: "add_to_cart",
  441. originating_snr: "1_store-navigation__",
  442. sessionid: document.cookie.replace(/(?:(?:^|.*;\s*)sessionid\s*\=\s*([^;]*).*$)|^.*$/, "$1"),
  443. snr: "1_5_9__403",
  444. }
  445. data[`${type}id`] = String(subID);
  446. let s = [];
  447. for (let k in data) {
  448. s += `${k}=${encodeURIComponent(data[k])}&`;
  449. }
  450. fetch('https://store.steampowered.com/cart/', {
  451. method: 'POST',
  452. credentials: 'include',
  453. body: s,
  454. headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' },
  455. })
  456. .then(async response => {
  457. if (response.ok) {
  458. let data = await response.text();
  459. if (appID !== null) {
  460. let reg = new RegExp('app\/' + appID);
  461. if (data.search(reg) !== -1) {
  462. resolve([true, '添加购物车成功']);
  463. }
  464. else {
  465. resolve([false, '添加购物车失败']);
  466. }
  467. } else {
  468. resolve([true, '添加购物车成功']);
  469. }
  470. } else {
  471. resolve([false, '网络请求失败']);
  472. }
  473. }).catch(err => {
  474. console.error(err);
  475. resolve([false, '未知错误:' + err]);
  476. });
  477. });
  478. }
  479. //显示提示
  480. function showAlert(title, text, succ = true) {
  481. return ShowAlertDialog(`${succ ? '✅' : '❌'}${title}`, text);
  482. }
  483. })();
  484.  
  485. GM_addStyle(`
  486. button.fac_listbtns {
  487. display: none;
  488. position: relative;
  489. z-index: 100;
  490. padding: 1px;
  491. }
  492. a.search_result_row > button.fac_listbtns {
  493. top: -25px;
  494. left: 300px;
  495. }
  496. a.tab_item > button.fac_listbtns {
  497. top: -40px;
  498. left: 330px;
  499. }
  500. a.recommendation_link > button.fac_listbtns {
  501. bottom: 10px;
  502. right: 10px;
  503. position: absolute;
  504. }
  505. div.wishlist_row > button.fac_listbtns {
  506. top: 35%;
  507. right: 30%;
  508. position: absolute;
  509. }
  510. div.game_purchase_action > button.fac_listbtns {
  511. right: 8px;
  512. bottom: 8px;
  513. }
  514. button.fac_cartbtns {
  515. padding: 5px 10px;
  516. }
  517. button.fac_cartbtns:not(:last-child) {
  518. margin-right: 7px;
  519. }
  520. button.fac_cartbtns:not(:first-child) {
  521. margin-left: 7px;
  522. }
  523. a.tab_item:hover button.fac_listbtns,
  524. a.search_result_row:hover button.fac_listbtns,
  525. div.recommendation:hover button.fac_listbtns,
  526. div.wishlist_row:hover button.fac_listbtns {
  527. display: block;
  528. }
  529. div.game_purchase_action:hover > button.fac_listbtns {
  530. display: inline;
  531. }
  532. button.fac_choose {
  533. padding: 1px;
  534. margin: 2px 5px;
  535. }
  536. textarea.fac_inputbox {
  537. height: 130px;
  538. resize: vertical;
  539. font-size: 10px;
  540. }
  541. `);

QingJ © 2025

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