// ==UserScript== // @name:zh-CN Steam快速添加购物车 // @name Fast_Add_Cart // @namespace https://blog.chrxw.com // @supportURL https://blog.chrxw.com/scripts.html // @contributionURL https://afdian.net/@chr233 // @version 2.14 // @description 超级方便的添加购物车体验,不用跳转商店页。 // @description:zh-CN 超级方便的添加购物车体验,不用跳转商店页。 // @author Chr_ // @match https://store.steampowered.com/* // @license AGPL-3.0 // @icon https://blog.chrxw.com/favicon.ico // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @grant GM_info // @downloadURL none // ==/UserScript== (async () => { 'use strict'; //初始化 let pathname = window.location.pathname; if (pathname === '/search/' || pathname === '/' || pathname.startsWith('/tags/')) { //搜索页,主页,标签页 let t = setInterval(() => { let containers = document.querySelectorAll([ '#search_resultsRows', '#tab_newreleases_content', '#tab_topsellers_content', '#tab_upcoming_content', '#tab_specials_content', '#NewReleasesRows', '#TopSellersRows', '#ConcurrentUsersRows', '#TopRatedRows', '#ComingSoonRows' ].join(',')); if (containers.length > 0) { for (let container of containers) { clearInterval(t); for (let ele of container.children) { addButton(ele); } container.addEventListener('DOMNodeInserted', ({ relatedNode }) => { if (relatedNode.parentElement === container) { addButton(relatedNode); } }); } } }, 500); } else if (pathname.startsWith('/publisher/') || pathname.startsWith('/franchise/')) { //发行商主页 let t = setInterval(() => { let container = document.getElementById('RecommendationsRows'); if (container != null) { clearInterval(t); for (let ele of container.querySelectorAll('a.recommendation_link')) { addButton(ele); } container.addEventListener('DOMNodeInserted', ({ relatedNode }) => { if (relatedNode.nodeName === 'DIV') { console.log(relatedNode); for (let ele of relatedNode.querySelectorAll('a.recommendation_link')) { addButton(ele); } } }); } }, 500); } else if (pathname.startsWith('/app/') || pathname.startsWith('/sub/') || pathname.startsWith('/bundle/')) { //商店详情页 let t = setInterval(() => { let container = document.getElementById('game_area_purchase'); if (container != null) { clearInterval(t); for (let ele of container.querySelectorAll('div.game_area_purchase_game')) { addButton2(ele); } } }, 500); } else if (pathname.startsWith('/wishlist/')) { //愿望单页 let t = setInterval(() => { let container = document.getElementById('wishlist_ctn'); if (container != null) { clearInterval(t); for (let ele of container.querySelectorAll('div.wishlist_row')) { addButton3(ele); } container.addEventListener('DOMNodeInserted', ({ relatedNode }) => { if (relatedNode.nodeName === 'DIV') { console.log(relatedNode); for (let ele of relatedNode.querySelectorAll('div.wishlist_row')) { addButton3(ele); } } }); } }, 500); } else if (pathname === '/cart/') { //购物车页 let continer = document.querySelector('div.cart_area_body'); let genBr = () => { return document.createElement('br'); }; let genBtn = (text, title, onclick) => { let btn = document.createElement('button'); btn.textContent = text; btn.title = title; btn.className = 'btn_medium btnv6_blue_hoverfade fac_cartbtns'; btn.addEventListener('click', onclick); return btn; }; let genSpan = (text) => { let span = document.createElement('span'); span.textContent = text; return span; }; let inputBox = document.createElement('textarea'); inputBox.value = GM_getValue('fac_cart') ?? ''; inputBox.className = 'fac_inputbox'; inputBox.placeholder = ['一行一条, 自动忽略【#】后面的内容, 支持的格式如下: (自动保存)', '1. 商店链接: https://store.steampowered.com/app/xxx', '2. DB链接: https://steamdb.info/app/xxx', '3. appID: xxx a/xxx app/xxx', '4. subID: s/xxx sub/xxx', '5. bundleID: b/xxx bundle/xxx' ].join('\n'); let btnArea = document.createElement('div'); let btnImport = genBtn('🔼批量导入', '从文本框批量添加购物车', async () => { inputBox.value = await importCart(inputBox.value); window.location.reload(); }); let btnExport = genBtn('🔽导出', '将购物车内容导出至文本框', () => { inputBox.value = exportCart(); }); let btnCopy = genBtn('📋复制', '复制文本框中的内容', () => { GM_setClipboard(inputBox.value, { type: 'text', mimetype: 'text/plain' }); showAlert('提示', '复制到剪贴板成功', true); }); let btnClear = genBtn('🗑️清除', '清除文本框和已保存的数据', () => { inputBox.value = ''; GM_setValue('fac_cart', ''); showAlert('提示', '文本框内容和保存的数据已清除', true); }); let btnForget = genBtn('⚠️清空', '清空购物车', () => { ShowConfirmDialog('', '您确定要移除所有您购物车中的物品吗?', '是', '否') .done(() => { ForgetCart(); window.location.reload(); }); }); let btnHelp = genBtn('🔣帮助', '显示帮助', () => { const { script: { version } } = GM_info; showAlert(`帮助 插件版本 ${version}`, [ '
【🔼批量导入】从文本框批量添加购物车。
', '【🔽导出】将购物车内容导出至文本框。
', '【📋复制】复制文本框中的内容(废话)。
', '【🗑️清除】清除文本框和已保存的数据。
', '【⚠️清空】清空购物车。
', '【🔣帮助】显示没什么卵用的帮助。
', '【发布帖】 【脚本反馈】 【Developed by Chr_】
' ].join('${lines.join('
')}
`, true); setTimeout(() => { d.Dismiss(); }, 1200); } dialog.Dismiss(); resolve(lines.join('\n')); }); } //导出购物车 function exportCart() { let data = []; let regMatch = new RegExp(/(app|sub|bundle)_(\d+)/); for (let item of document.querySelectorAll('div.cart_item_list>div.cart_row ')) { let itemKey = item.getAttribute('data-ds-itemkey'); let name = item.querySelector('.cart_item_desc').textContent.trim(); let match = itemKey.toLowerCase().match(regMatch); if (match) { let [_, type, id] = match; data.push(`${type}/${id} #${name}`); } } return data.join('\n'); } //添加按钮 function addButton(element) { if (element.getAttribute('added') !== null) { return; } element.setAttribute('added', ''); if (element.href === undefined) { return; } let appID = (element.href.match(/\/app\/(\d+)/) ?? [null, null])[1]; if (appID === null) { return; } let btn = document.createElement('button'); btn.addEventListener('click', (e) => { chooseSubs(appID); e.preventDefault(); }, false); btn.className = 'fac_listbtns'; btn.textContent = '🛒'; element.appendChild(btn); } //添加按钮 function addButton2(element) { if (element.getAttribute('added') !== null) { return; } element.setAttribute('added', ''); let type, subID; let parentElement = element.parentElement; if (parentElement.hasAttribute('data-ds-itemkey')) { let itemKey = parentElement.getAttribute('data-ds-itemkey'); let match = itemKey.toLowerCase().match(/(app|sub|bundle)_(\d+)/); if (match) { [, type, subID] = match; } } else if (parentElement.hasAttribute('data-ds-bundleid') || parentElement.hasAttribute('data-ds-subid')) { subID = parentElement.getAttribute('data-ds-subid') ?? parentElement.getAttribute('data-ds-bundleid'); type = parentElement.hasAttribute('data-ds-subid') ? 'sub' : 'bundle'; } else { let match = element.id.match(/cart_(\d+)/); if (match) { type = 'sub'; [, subID] = match; } } if (type === undefined || subID === undefined) { console.log('未识别到subID'); return; } const btnBar = element.querySelector('div.game_purchase_action'); const firstItem = element.querySelector('div.game_purchase_action_bg'); if (btnBar === null || firstItem == null || type === undefined || subID === undefined) { return; } let appID = (window.location.pathname.match(/\/(app)\/(\d+)/) ?? [null, null, null])[2]; let btn = document.createElement('button'); btn.addEventListener('click', async () => { let dialog = showAlert('操作中……', '添加到购物车……
', true); let [succ, msg] = await addCart(type, subID, appID); let done = showAlert('操作完成', `${msg}
`, succ); setTimeout(() => { done.Dismiss(); }, 1200); dialog.Dismiss(); if (succ) { let acBtn = btnBar.querySelector('div[class="btn_addtocart"]>a'); if (acBtn) { acBtn.href = 'https://store.steampowered.com/cart/'; acBtn.innerHTML = '\n\t\n在购物车中\n\t\n'; } } }, false); btn.className = 'fac_listbtns'; btn.textContent = '🛒'; btnBar.insertBefore(btn, firstItem); } //添加按钮 function addButton3(element) { if (element.getAttribute('added') !== null) { return; } element.setAttribute('added', ''); let appID = element.getAttribute('data-app-id'); if (appID === null) { return; } let btn = document.createElement('button'); btn.addEventListener('click', (e) => { chooseSubs(appID); e.preventDefault(); }, false); btn.className = 'fac_listbtns'; btn.textContent = '🛒'; element.appendChild(btn); } //选择SUB async function chooseSubs(appID) { let dialog = showAlert('操作中……', '读取可用SUB
', true); getGameSubs(appID) .then(async (subInfos) => { if (subInfos.length === 0) { showAlert('添加购物车失败', '未找到可用SUB, 可能尚未发行或者是免费游戏.
', false); dialog.Dismiss(); return; } else { console.log(subInfos); if (subInfos.length === 1) { let [subID, subName, discount, price] = subInfos[0]; await addCart('sub', subID, appID); let done = showAlert('添加购物车成功', `${subName} - ${price}
`, true); setTimeout(() => { done.Dismiss(); }, 1200); dialog.Dismiss(); } else { let dialog2 = showAlert('请选择SUB', '', true); dialog.Dismiss(); await new Promise((resolve) => { let t = setInterval(() => { if (document.getElementById('fac_choose') !== null) { clearInterval(t); resolve(); } }, 200); }); let divContiner = document.getElementById('fac_choose'); for (let [subID, subName, discount, price] of subInfos) { let btn = document.createElement('button'); btn.addEventListener('click', async () => { let dialog = showAlert('操作中……', `添加 ${subName} - ${price} 到购物车
`, true); dialog2.Dismiss(); let [succ, msg] = await addCart('sub', subID, appID); let done = showAlert(msg, `${subName} - ${price}
`, succ); setTimeout(() => { done.Dismiss(); }, 1200); dialog.Dismiss(); }); btn.textContent = '🛒添加购物车'; btn.className = 'fac_choose'; let p = document.createElement('p'); p.textContent = `${subName} - ${price}`; p.appendChild(btn); divContiner.appendChild(p); } } } }) .catch(err => { let done = showAlert('网络错误', `${err}
`, false); setTimeout(() => { done.Dismiss(); }, 2000); dialog.Dismiss(); }); } //读取sub信息 function getGameSubs(appID) { return new Promise((resolve, reject) => { const regPure = new RegExp(/ - [^-]*$/, ''); const regSymbol = new RegExp(/[> ] (.+) \d/, ''); const lang = document.cookie.replace(/(?:(?:^|.*;\s*)Steam_Language\s*\=\s*([^;]*).*$)|^.*$/, "$1") fetch(`https://store.steampowered.com/api/appdetails?appids=${appID}&lang=${lang}`, { method: 'GET', credentials: 'include', }) .then(async response => { if (response.ok) { let data = await response.json(); let result = data[appID]; if (result.success !== true) { reject('返回了未知结果'); } let subInfos = []; for (let pkg of result.data.package_groups) { for (let sub of pkg.subs) { const { packageid, option_text, percent_savings_text, price_in_cents_with_discount } = sub; if (price_in_cents_with_discount > 0) { //排除免费SUB let symbol = option_text.match(regSymbol)?.pop(); let price = price_in_cents_with_discount / 100 + ' ' + symbol; let subName = option_text.replace(regPure, ''); if (percent_savings_text === ' ') { subInfos.push([packageid, subName, percent_savings_text, price]); } else { subInfos.push([packageid, subName, false, price]); } } } } resolve(subInfos); } else { reject('网络请求失败'); } }).catch(err => { reject(err); }); }); } //添加购物车,只支持subID和bundleID function addCart(type = 'sub', subID, appID = null) { window.localStorage['fac_subid'] = subID; return new Promise((resolve, reject) => { let data = { action: "add_to_cart", originating_snr: "1_store-navigation__", sessionid: document.cookie.replace(/(?:(?:^|.*;\s*)sessionid\s*\=\s*([^;]*).*$)|^.*$/, "$1"), snr: "1_5_9__403", } data[`${type}id`] = String(subID); let s = []; for (let k in data) { s += `${k}=${encodeURIComponent(data[k])}&`; } fetch('https://store.steampowered.com/cart/', { method: 'POST', credentials: 'include', body: s, headers: { 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }, }) .then(async response => { if (response.ok) { let data = await response.text(); if (appID !== null) { let reg = new RegExp('app\/' + appID); if (data.search(reg) !== -1) { resolve([true, '添加购物车成功']); } else { resolve([false, '添加购物车失败']); } } else { resolve([true, '添加购物车成功']); } } else { resolve([false, '网络请求失败']); } }).catch(err => { console.error(err); resolve([false, '未知错误:' + err]); }); }); } //显示提示 function showAlert(title, text, succ = true) { return ShowAlertDialog(`${succ ? '✅' : '❌'}${title}`, text); } })(); GM_addStyle(` button.fac_listbtns { display: none; position: relative; z-index: 100; padding: 1px; } a.search_result_row > button.fac_listbtns { top: -25px; left: 300px; } a.tab_item > button.fac_listbtns { top: -40px; left: 330px; } a.recommendation_link > button.fac_listbtns { bottom: 10px; right: 10px; position: absolute; } div.wishlist_row > button.fac_listbtns { top: 35%; right: 30%; position: absolute; } div.game_purchase_action > button.fac_listbtns { right: 8px; bottom: 8px; } button.fac_cartbtns { padding: 5px 10px; } button.fac_cartbtns:not(:last-child) { margin-right: 7px; } button.fac_cartbtns:not(:first-child) { margin-left: 7px; } a.tab_item:hover button.fac_listbtns, a.search_result_row:hover button.fac_listbtns, div.recommendation:hover button.fac_listbtns, div.wishlist_row:hover button.fac_listbtns { display: block; } div.game_purchase_action:hover > button.fac_listbtns { display: inline; } button.fac_choose { padding: 1px; margin: 2px 5px; } textarea.fac_inputbox { height: 130px; resize: vertical; font-size: 10px; } `);