// ==UserScript==
// @name SteamPy Plus
// @name:zh-CN SteamPy Plus
// @name:en SteamPy Plus
// @namespace http://github.com/blue-bird1/tampermonkey-script
// @version 4.10
// @description 增强购买Steampy密钥的体验,增加筛选功能,支持鼠标中键打开Steam页面。
// @description:en Enhance the experience of purchasing Steampy keys, add filter functionality, and support opening Steam pages with the middle mouse button. // 英文描述(可选,补充默认描述的英文版本)
// @author 豆包 (Doubao)
// @match https://steampy.com/*
// @grant GM_setValue
// @grant GM_getValue
// @icon https://steampy.com/logo.ico
// @require https://scriptcat.org/lib/637/1.4.8/ajaxHooker.js#sha256=dTF50feumqJW36kBpbf6+LguSLAtLr7CEs3oPmyfbiM=
// @require https://scriptcat.org/lib/513/2.1.0/ElementGetter.js#sha256=aQF7JFfhQ7Hi+weLrBlOsY24Z2ORjaxgZNoni7pAz5U=
// @run-at document-start
// @license MIT
// ==/UserScript==
/*global elmGetter,ajaxHooker*/
(function () {
'use strict';
// 状态管理
const StateManager = {
saveState(state) {
GM_setValue('steamPriceFilterState', JSON.stringify(state));
},
loadState() {
const saved = GM_getValue('steamPriceFilterState', null);
return saved ? JSON.parse(saved) : {
minPrice: 0,
maxPrice: 9999,
isActive: false
};
}
};
let filterState = StateManager.loadState();
// 修复后的工具函数:提取游戏ID(优先读取data-src)
function getSteamAppId(gameBlock) {
const iconImg = gameBlock.querySelector('.cdkGameIcon');
if (!iconImg) return null;
// 优先读取真实图片地址(data-src),再兼容src
const imgUrl = iconImg.dataset.src || iconImg.src;
// 从图片地址中匹配游戏ID(例如从steam/apps/1651560/中提取1651560)
const match = imgUrl.match(/steam\/apps\/(\d+)\/header/);
return match ? match[1] : null;
}
// 游戏数据存储
const TempDataStore = {
steamGameData: null,
setGameData(data) {
this.steamGameData = data;
},
getGameData() {
return this.steamGameData || { result: { content: [] } };
},
getRatingByAppId(appId) {
const gameList = this.getGameData().result.content;
const targetGame = gameList.find(game => game.appId === appId);
return targetGame?.rating || 0;
}
};
// 接口拦截
ajaxHooker.hook(request => {
if (request.url.includes('/xboot/steamGame/keyHot')) {
request.response = (res) => {
try {
const originalData = JSON.parse(res.responseText);
TempDataStore.setGameData(originalData);
res.responseText = JSON.stringify(originalData);
} catch (e) {
console.error('接口数据处理失败:', e);
}
};
}
return request;
});
// 单个游戏评分更新(使用Steam风格文本描述)
function updateGameRating(gameBlock) {
if (!gameBlock) return;
const appId = getSteamAppId(gameBlock);
const gameHead = gameBlock.querySelector('.gameHead');
// 只有存在有效ID时才处理评分
if (appId && gameHead) {
const rating = TempDataStore.getRatingByAppId(appId);
const ratingEl = gameHead.querySelector('.gameRating');
// 有评分数据
if (rating > 0) {
// 计算百分比并映射到Steam评分等级
const ratingPercent = Math.round(rating * 100);
let ratingText, ratingClass;
// Steam风格评分标准
if (ratingPercent >= 90) {
ratingText = "好评如潮";
ratingClass = "overwhelmingly-positive";
} else if (ratingPercent >= 80) {
ratingText = "特别好评";
ratingClass = "very-positive";
} else if (ratingPercent >= 70) {
ratingText = "多半好评";
ratingClass = "positive";
} else if (ratingPercent >= 40) {
ratingText = "褒贬不一";
ratingClass = "mixed";
} else if (ratingPercent >= 20) {
ratingText = "多半差评";
ratingClass = "negative";
} else {
ratingText = "特别差评";
ratingClass = "very-negative";
}
if (ratingEl) {
// 只在内容变化时更新
if (ratingEl.textContent !== ratingText) {
ratingEl.textContent = ratingText;
}
// 更新评分等级类名
if (!ratingEl.classList.contains(ratingClass)) {
ratingEl.classList.remove(
'overwhelmingly-positive',
'very-positive',
'positive',
'mixed',
'negative',
'very-negative'
);
ratingEl.classList.add(ratingClass);
}
} else {
// 创建新评分标签
const newRatingEl = document.createElement('div');
newRatingEl.className = `gameRating ${ratingClass}`;
newRatingEl.textContent = ratingText;
gameHead.appendChild(newRatingEl);
}
}
// 无评分数据则移除标签
else if (ratingEl) {
ratingEl.remove();
}
}
// 无ID时移除现有评分标签
else if (gameHead) {
const ratingEl = gameHead.querySelector('.gameRating');
if (ratingEl) ratingEl.remove();
}
}
// 同步更新评分样式
function injectRatingStyle() {
const existingStyle = document.getElementById('ratingStyle');
if (existingStyle) {
existingStyle.remove();
}
const style = document.createElement('style');
style.id = 'ratingStyle';
style.textContent = `
.gameHead .gameRating {
padding: 0 8px !important;
height: .3rem !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
color: #fff !important;
text-align: center !important;
line-height: .3rem !important;
border-radius: .09rem 0 0 0 !important;
font-size: .12rem !important;
font-weight: bold !important;
z-index: 10 !important;
white-space: nowrap !important;
}
/* Steam风格评分颜色 */
.gameRating.overwhelmingly-positive { background: #4CAF50 !important; } /* 好评如潮 - 深绿 */
.gameRating.very-positive { background: #8BC34A !important; } /* 特别好评 - 中绿 */
.gameRating.positive { background: #CDDC39 !important; color: #333 !important; } /* 多半好评 - 浅绿 */
.gameRating.mixed { background: #FFC107 !important; color: #333 !important; } /* 褒贬不一 - 黄色 */
.gameRating.negative { background: #FF9800 !important; } /* 多半差评 - 橙色 */
.gameRating.very-negative { background: #F44336 !important; } /* 特别差评 - 红色 */
`;
document.head.appendChild(style);
}
// 等待元素加载
function waitForElement(selector, callback, timeout = 10000) {
const start = Date.now();
const timer = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(timer);
callback(el);
} else if (Date.now() - start > timeout) {
clearInterval(timer);
console.warn(`超时未找到元素: ${selector}`);
insertFilterUI();
}
}, 200);
}
// 筛选UI
function createFilterUI() {
const ui = document.createElement('div');
ui.id = 'priceFilterContainer';
ui.className = 'ml-5-rem c-point tagBtnTwo flex-row align-items-center';
ui.style.cssText = `font-family:Arial,sans-serif;font-size:13px;align-items:center;gap:8px;padding:8px;z-index:9999;position:relative;background:#f9f9f9;border-radius:4px;border:1px solid #eee;height:.25rem;`;
const title = document.createElement('span');
title.className = 'tag-titleOne ml-3-rem';
title.textContent = '价格筛选';
title.style.fontWeight = 'bold';
ui.appendChild(title);
const presets = [
{ text: '0-20元', min: 0, max: 20 },
{ text: '20元以上', min: 20, max: 9999 }
];
const presetContainer = document.createElement('div');
presetContainer.className = 'flex-row jc-space-flex-start align-items-center pr5-rem';
presetContainer.style.gap = '8px';
presets.forEach(p => {
const btn = document.createElement('div');
btn.className = 'tagBtn';
btn.dataset.min = p.min;
btn.dataset.max = p.max;
btn.textContent = p.text;
btn.style.cssText = `padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #ddd;color:#666;background:transparent;transition:all 0.2s;`;
if (filterState.isActive && filterState.minPrice === p.min && filterState.maxPrice === p.max) {
btn.style.cssText = `padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #409EFF;color:#fff;background:#409EFF;transition:all 0.2s;`;
}
btn.onclick = () => {
filterState.minPrice = p.min;
filterState.maxPrice = p.max;
filterState.isActive = true;
StateManager.saveState(filterState);
syncInputValues();
applyFilter(); // 仅应用筛选,不更新评分
updatePresetHighlights();
};
presetContainer.appendChild(btn);
});
ui.appendChild(presetContainer);
const inputContainer = document.createElement('div');
inputContainer.className = 'flex-row align-items-center';
inputContainer.style.gap = '8px';
const minInp = document.createElement('input');
minInp.id = 'priceFilterMin';
minInp.type = 'number';
minInp.placeholder = '最低价';
minInp.min = 0;
minInp.step = 0.01;
minInp.style.cssText = `width:70px;height:28px;padding:0 8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:13px;`;
minInp.addEventListener('input', (e) => {
filterState.minPrice = parseFloat(e.target.value) || 0;
filterState.isActive = true;
StateManager.saveState(filterState);
});
const maxInp = document.createElement('input');
maxInp.id = 'priceFilterMax';
maxInp.type = 'number';
maxInp.placeholder = '最高价';
maxInp.min = 0;
maxInp.step = 0.01;
maxInp.style.cssText = `width:70px;height:28px;padding:0 8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:13px;`;
maxInp.addEventListener('input', (e) => {
filterState.maxPrice = parseFloat(e.target.value) || 9999;
filterState.isActive = true;
StateManager.saveState(filterState);
});
const filterBtn = document.createElement('button');
filterBtn.className = 'ivu-btn ivu-btn-default ivu-btn-sm';
filterBtn.textContent = '筛选';
filterBtn.style.cssText = `margin-left:4px;padding:4px 12px;cursor:pointer;background:#409EFF;color:white;border:1px solid #409EFF;border-radius:4px;`;
filterBtn.onclick = () => {
applyFilter(); // 仅应用筛选,不更新评分
updatePresetHighlights(false);
};
inputContainer.append(minInp, document.createTextNode('-'), maxInp, filterBtn);
ui.appendChild(inputContainer);
return ui;
}
function insertFilterUI() {
if (document.getElementById('priceFilterContainer')) return;
const ui = createFilterUI();
const targetContainer = document.querySelector('.tag.flex-row.align-items-center');
if (targetContainer) {
targetContainer.appendChild(ui);
syncInputValues();
}
if (filterState.isActive) {
applyFilter();
}
}
function updatePresetHighlights(shouldHighlight = true) {
document.querySelectorAll('.tagBtn[data-min]').forEach(btn => {
const btnMin = parseFloat(btn.dataset.min);
const btnMax = parseFloat(btn.dataset.max);
const isMatch = filterState.isActive && filterState.minPrice === btnMin && filterState.maxPrice === btnMax;
btn.style.cssText = shouldHighlight && isMatch
? `padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #409EFF;color:#fff;background:#409EFF;transition:all 0.2s;`
: `padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #ddd;color:#666;background:transparent;transition:all 0.2s;`;
});
}
// 价格筛选核心逻辑
function syncInputValues() {
const minInp = document.getElementById('priceFilterMin');
const maxInp = document.getElementById('priceFilterMax');
if (minInp && filterState.isActive) minInp.value = filterState.minPrice;
if (maxInp && filterState.isActive) maxInp.value = filterState.maxPrice;
}
function getGamePrice(gameBlock) {
const priceEl = gameBlock.querySelector('.gamePrice');
if (!priceEl) return 0;
const priceText = priceEl.textContent.replace(/[¥元]/g, '').trim().toLowerCase();
return priceText === '免费' ? 0 : (parseFloat(priceText) || 0);
}
function processGame(gameBlock) {
if (gameBlock.dataset.filterProcessed) return;
gameBlock.dataset.filterProcessed = 'true';
gameBlock.addEventListener('mousedown', e => {
if (e.button === 1 && !e.ctrlKey && !e.shiftKey) {
const appId = getSteamAppId(gameBlock);
if (appId) {
e.preventDefault();
window.open(`https://store.steampowered.com/app/${appId}/`, '_blank');
}
}
});
applyFilterToGame(gameBlock);
}
// 应用筛选到单个游戏(仅在状态变化且变为可见时才更新评分)
function applyFilterToGame(gameBlock, forceRatingUpdate = false) {
if (!gameBlock) return false;
const price = getGamePrice(gameBlock);
const shouldShow = !filterState.isActive ||
(price >= filterState.minPrice && price <= filterState.maxPrice);
const wasShowing = gameBlock.style.display !== 'none';
// 状态变化时才更新DOM
if (shouldShow !== wasShowing) {
gameBlock.style.display = shouldShow ? 'block' : 'none';
// 只有变为可见状态时才可能需要更新评分
if (shouldShow) {
updateGameRating(gameBlock);
}
return true;
}
// 强制更新评分(用于ID变化的情况)
if (forceRatingUpdate && shouldShow) {
updateGameRating(gameBlock);
}
return false;
}
// 批量应用筛选(不主动更新评分)
function applyFilter() {
document.querySelectorAll('.gameblock:not([data-filter-processed])')
.forEach(processGame);
let hasChanges = false;
document.querySelectorAll('.gameblock').forEach(gameBlock => {
if (applyFilterToGame(gameBlock)) {
hasChanges = true;
}
});
// 处理空状态显示
const visibleCount = 35 - document.querySelectorAll('.gameblock[style="display: none;"]').length;
const emptyMsg = document.querySelector('.tc.mt-50-rem.pb-20-rem');
if (emptyMsg) {
emptyMsg.style.display = visibleCount === 0 ? 'block' : 'none';
}
return hasChanges;
}
// 精准监控器:仅在游戏ID变化时更新评分
async function startContentMonitor() {
const gameContainer = await elmGetter.get('.ivu-tabs-content');
if (!gameContainer) {
console.warn('未找到游戏容器,1秒后重试');
setTimeout(startContentMonitor, 1000);
return;
}
const observerConfig = {
subtree: true,
attributes: true,
attributeFilter: ['src', 'style'],
characterData: true
};
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
let gameBlock = null;
let requiresRatingUpdate = false;
// 游戏图标变化(ID可能改变)- 需要更新评分
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
if (mutation.target.classList?.contains('cdkGameIcon')) {
gameBlock = mutation.target.closest('.gameblock');
requiresRatingUpdate = true; // 图片变化意味着ID可能变化
}
}
// 价格文本变化 - 只影响筛选,不更新评分
else if (mutation.type === 'characterData') {
const priceEl = mutation.target.parentElement;
if (priceEl && priceEl.classList.contains('gamePrice')) {
gameBlock = priceEl.closest('.gameblock');
requiresRatingUpdate = false; // 价格变化不影响评分
}
}
// 处理找到的游戏块
if (gameBlock) {
if (requiresRatingUpdate) {
// ID变化或变为可见,需要更新评分
applyFilterToGame(gameBlock, true);
} else {
// 仅应用筛选,不更新评分
applyFilterToGame(gameBlock, false);
}
}
});
});
observer.observe(gameContainer, observerConfig);
console.log('监控器已启动');
window.addEventListener('beforeunload', () => {
observer.disconnect();
});
// 初始加载后执行一次
setTimeout(() => {
applyFilter();
// 初始加载时对所有可见游戏更新评分
document.querySelectorAll('.gameblock')
.forEach(gameBlock => updateGameRating(gameBlock));
}, 600);
}
// 路径处理
const TARGET_PATH = '/cdKey/cdKey';
let isInitialized = false;
function isTargetPath() {
return window.location.pathname.startsWith(TARGET_PATH);
}
function cleanUp() {
if (!isInitialized) return;
isInitialized = false;
}
function handlePathChange() {
if (isTargetPath() && !isInitialized) {
console.log("run script in path")
init();
} else if (!isTargetPath() && isInitialized) {
cleanUp();
}
}
async function init() {
if (isInitialized) return;
injectRatingStyle();
waitForElement('.tag.flex-row.align-items-center', insertFilterUI);
await startContentMonitor();
isInitialized = true;
}
// 监听历史变化
let lastPath = location.pathname + location.search;
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
const newPath = location.pathname + location.search;
if (newPath !== lastPath) {
lastPath = newPath;
handlePathChange();
}
};
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
const newPath = location.pathname + location.search;
if (newPath !== lastPath) {
lastPath = newPath;
handlePathChange();
}
};
window.addEventListener('popstate', handlePathChange);
window.addEventListener('hashchange', handlePathChange);
// 初始检查
if (isTargetPath()) {
init();
}
})();