Better SteamPY基础上增加了绝版游戏筛选、自动翻页、慈善包筛选功能
// ==UserScript==
// @name Better SteamPYPlus
// @namespace https://space.bilibili.com/93654843
// @version 1.0.3
// @description Better SteamPY基础上增加了绝版游戏筛选、自动翻页、慈善包筛选功能
// @author FiNNiER、ZY
// @match *://steampy.com/*
// @icon https://steampy.com/img/logo.63413a4f.png
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @connect api.steampowered.com
// @connect store.steampowered.com
// @connect steam-tracker.com
// @connect barter.vg
// @connect bartervg.com
// @run-at document-body
// ==/UserScript==
var Saves = {
wishlist: [],
ownedApps: [],
familygameList: [],
lastupdatetime: 0,
};
var limitedApps = [];
var limitedAppsSet = new Set();
var noGameList = [];
var noGameListSet = new Set();
var delistedGamesData = null; // 存储下架游戏数据
var noDlc = false;
var noownedGames = false;
var noRestrictedGames = false;
var onlyDelistedGames = false; // 只显示下架游戏
var delistedTypes = []; // 选中的下架类型
var autoSkipTimer = null; // 用于防抖的计时器
var isAutoSkipping = false; // 防止重复触发翻页
var lastGameIds = ''; // 记录上一次的游戏ID列表,用于检测数据是否更新
var autoSkipTargetPage = 0; // 自动跳页的目标页码
var isFastScanning = false; // 快速扫描进行中
var fastScanAbort = false; // 中止快速扫描
var blacklist = []; // 黑名单 [{appId, name, addedTime}]
var noBlacklistGames = false; // 不显示黑名单游戏
var noBundledGames = false; // 不显示进过慈善包的游戏
var scanHideAllGames = false; // 扫描模式:隐藏全部当前结果,用于收集待确认 appid
var bundledAppsData = null; // barter.vg 慈善包数据 {appid: bundleCount}
var profileConfirmDB = {}; // 本地状态库 {appid: {status, appType, checkedAt}}
var profileConfirmQueue = []; // 等待写入本地状态库的 appid
var profileConfirmSaveTimer = null;
var profileConfirmScanTimer = null;
var cdkeyCheckQueue = new Set();
var cdkeyCheckTimer = null;
var profileConfirmScanning = false;
var profileConfirmAbort = false;
var profileConfirmAutoScan = false;
var settingsVm = null;
const PROFILE_CONFIRM_DB_KEY = 'ProfileConfirmDB';
const PROFILE_CONFIRM_QUEUE_KEY = 'ProfileConfirmQueue';
const PROFILE_CONFIRM_AUTO_SCAN_KEY = 'ProfileConfirmAutoScan';
const PROFILE_CONFIRM_AUTO_SCAN_MIGRATED_KEY = 'ProfileConfirmAutoScanDefaultOff';
const PROFILE_CONFIRM_CONCURRENT = 6;
const PROFILE_CONFIRM_BATCH_DELAY = 200;
const PROFILE_CONFIRM_TIMEOUT = 8000;
const PROFILE_CONFIRM_EXPORT_VERSION = 1;
(function () {
'use strict';
load();
observePageChanges();
})();
// 判断单个 appId 是否符合当前筛选条件(纯数据判断,不依赖 DOM)
function doesAppPassFilter(appId) {
appId = Number(appId);
if (!appId) return false;
// 下架游戏筛选
if (onlyDelistedGames) {
if (!delistedGamesData || !delistedGamesData.removed_apps) return false;
const gameData = delistedGamesData.removed_apps[appId.toString()];
if (!gameData) return false;
const categoryMap = {
'Purchase disabled': 'purchase disabled',
'Delisted': 'delisted',
'Delisted video': 'delisted',
'F2P': 'f2p',
'Unreleased': 'unreleased',
'Test app': 'test app',
'Retail only': 'retail only',
'Pre-order exclusive': 'pre-order exclusive',
'Banned': 'banned'
};
const mappedType = categoryMap[gameData.category] || (gameData.category || '').toLowerCase();
if (!delistedTypes.includes(mappedType)) return false;
}
// 不显示 DLC
if (noDlc && isNoGameApp(appId)) return false;
// 不显示已拥有
if (noownedGames && Saves.ownedApps.includes(appId)) return false;
rememberProfileConfirmCandidate(appId);
if (scanHideAllGames) return false;
// Hide only apps confirmed by the local profile-status DB.
if (noRestrictedGames && localStorage.getItem('IsProfileFeatureLimited') && isProfileRestrictedByConfirmedStatus(appId)) return false;
// 不显示黑名单
if (noBlacklistGames && blacklist.some(b => b.appId === appId)) return false;
// 不显示进过慈善包的游戏
if (noBundledGames && bundledAppsData) {
const bundleCount = bundledAppsData[appId.toString()];
if (bundleCount && bundleCount > 0) return false;
}
return true;
}
// 黑名单操作
function addToBlacklist(appId, name) {
appId = Number(appId);
if (blacklist.some(b => b.appId === appId)) return;
blacklist.push({ appId, name: name || ('App ' + appId), addedTime: Date.now() });
GM_setValue('Blacklist', blacklist);
}
function removeFromBlacklist(appId) {
appId = Number(appId);
blacklist = blacklist.filter(b => b.appId !== appId);
GM_setValue('Blacklist', blacklist);
}
function isInBlacklist(appId) {
return blacklist.some(b => b.appId === Number(appId));
}
function setLimitedApps(list) {
limitedApps = Array.isArray(list) ? list.map(Number).filter(id => id > 0) : [];
limitedAppsSet = new Set(limitedApps);
}
function rebuildLocalLimitedApps() {
const localNormalApps = Object.keys(profileConfirmDB || {})
.filter((appid) => {
const entry = profileConfirmDB[appid];
return entry && entry.status === 'normal' && entry.appType !== 'dlc' && entry.appType !== 'non_game';
})
.map(Number)
.filter(id => id > 0);
setLimitedApps(localNormalApps);
GM_setValue('limitedApps', limitedApps);
return limitedApps;
}
function isBaseNormalApp(appId) {
return limitedAppsSet.has(Number(appId));
}
function isOwnedApp(appId) {
return Saves.ownedApps.includes(Number(appId));
}
function setNoGameList(list) {
noGameList = Array.isArray(list) ? list.map(Number).filter(id => id > 0) : [];
noGameListSet = new Set(noGameList);
}
function isNoGameApp(appId) {
const appType = getLocalAppType(appId);
return appType === 'dlc' || appType === 'non_game';
}
function isUnavailableProfileStatus(status) {
return status === 'restricted' || status === 'learning' || status === 'banned';
}
function getProfileConfirmEntry(appId) {
return profileConfirmDB[String(Number(appId))] || null;
}
function getProfileConfirmStatus(appId) {
const entry = getProfileConfirmEntry(appId);
return entry && entry.status ? entry.status : null;
}
function getLocalAppType(appId) {
const entry = getProfileConfirmEntry(appId);
return entry && entry.appType ? entry.appType : null;
}
function isLocalStatusComplete(appId) {
const entry = getProfileConfirmEntry(appId);
return !!(entry && entry.status && entry.appType);
}
function rebuildLocalNoGameList() {
const localNoGameApps = Object.keys(profileConfirmDB || {})
.filter((appid) => {
const appType = profileConfirmDB[appid] && profileConfirmDB[appid].appType;
return appType === 'dlc' || appType === 'non_game';
})
.map(Number)
.filter(id => id > 0);
setNoGameList(localNoGameApps);
GM_setValue('NoGameList', noGameList);
return noGameList;
}
function isValidProfileConfirmStatus(status) {
return status === 'normal' || status === 'restricted' || status === 'learning' || status === 'banned';
}
function isValidLocalAppType(appType) {
return appType === 'game' || appType === 'dlc' || appType === 'non_game';
}
function isProfileRestrictedByConfirmedStatus(appId) {
if (isBannedApp(appId)) return true;
const status = getProfileConfirmStatus(appId);
if (status) return isUnavailableProfileStatus(status);
return false;
}
function getDelistedCategory(appId) {
if (!delistedGamesData || !delistedGamesData.removed_apps) return null;
const gameData = delistedGamesData.removed_apps[String(Number(appId))];
if (!gameData || !gameData.category) return null;
const categoryMap = {
'Purchase disabled': 'purchase disabled',
'Delisted': 'delisted',
'Delisted video': 'delisted',
'F2P': 'f2p',
'Unreleased': 'unreleased',
'Test app': 'test app',
'Retail only': 'retail only',
'Pre-order exclusive': 'pre-order exclusive',
'Banned': 'banned'
};
return categoryMap[gameData.category] || String(gameData.category).toLowerCase();
}
function isBannedApp(appId) {
return getDelistedCategory(appId) === 'banned';
}
function saveProfileConfirmDataSoon() {
if (profileConfirmSaveTimer) return;
profileConfirmSaveTimer = setTimeout(() => {
profileConfirmSaveTimer = null;
normalizeProfileConfirmQueue();
rebuildLocalLimitedApps();
rebuildLocalNoGameList();
GM_setValue(PROFILE_CONFIRM_DB_KEY, profileConfirmDB);
GM_setValue(PROFILE_CONFIRM_QUEUE_KEY, profileConfirmQueue);
updateProfileConfirmStats();
}, 500);
}
function saveProfileConfirmDataNow() {
if (profileConfirmSaveTimer) {
clearTimeout(profileConfirmSaveTimer);
profileConfirmSaveTimer = null;
}
normalizeProfileConfirmQueue();
rebuildLocalLimitedApps();
rebuildLocalNoGameList();
GM_setValue(PROFILE_CONFIRM_DB_KEY, profileConfirmDB);
GM_setValue(PROFILE_CONFIRM_QUEUE_KEY, profileConfirmQueue);
updateProfileConfirmStats();
}
function getProfileConfirmStats() {
const stats = {
total: 0,
normal: 0,
restricted: 0,
learning: 0,
banned: 0,
dlc: 0,
nonGame: 0,
game: 0,
unknown: 0,
queued: profileConfirmQueue.length
};
Object.values(profileConfirmDB || {}).forEach((entry) => {
stats.total++;
const status = entry && entry.status;
const appType = entry && entry.appType;
if (status === 'banned') {
stats.banned++;
stats.restricted++;
}
else if (stats[status] !== undefined) stats[status]++;
if (appType === 'dlc') stats.dlc++;
else if (appType === 'non_game') stats.nonGame++;
else if (appType === 'game') stats.game++;
else stats.unknown++;
});
return stats;
}
function normalizeProfileConfirmQueue() {
const seen = new Set();
profileConfirmQueue = (profileConfirmQueue || []).map(Number).filter((appId) => {
if (!appId || seen.has(appId) || isOwnedApp(appId) || isNoGameApp(appId) || isLocalStatusComplete(appId)) return false;
seen.add(appId);
return true;
});
}
function updateProfileConfirmStats() {
if (!settingsVm) return;
const stats = getProfileConfirmStats();
settingsVm.profileConfirmTotal = stats.total;
settingsVm.profileConfirmAddable = limitedApps.length;
settingsVm.profileConfirmNormal = stats.normal;
settingsVm.profileConfirmRestricted = stats.restricted;
settingsVm.profileConfirmLearning = stats.learning;
settingsVm.profileConfirmBanned = stats.banned;
settingsVm.profileConfirmDlc = stats.dlc;
settingsVm.profileConfirmNonGame = stats.nonGame;
settingsVm.profileConfirmUnknown = stats.unknown;
settingsVm.profileConfirmQueued = stats.queued;
settingsVm.profileConfirmScanning = profileConfirmScanning;
settingsVm.profileConfirmAutoScan = profileConfirmAutoScan;
}
function ensureProfileConfirmAutoScan(delay = 1000) {
normalizeProfileConfirmQueue();
updateProfileConfirmStats();
if (!profileConfirmAutoScan || profileConfirmQueue.length === 0 || profileConfirmScanning) return;
scheduleProfileConfirmScan(delay);
}
function rememberProfileConfirmCandidate(appId, force = false) {
appId = Number(appId);
if (isOwnedApp(appId)) return;
if (isNoGameApp(appId)) return;
if (!appId) return;
if (isLocalStatusComplete(appId)) return;
if (profileConfirmQueue.includes(appId)) return;
profileConfirmQueue.push(appId);
saveProfileConfirmDataSoon();
if (force || profileConfirmAutoScan) scheduleProfileConfirmScan(200, force);
}
function rememberProfileConfirmCandidates(appIds, force = false) {
let changed = false;
(appIds || []).forEach((appId) => {
appId = Number(appId);
if (isOwnedApp(appId)) return;
if (isNoGameApp(appId)) return;
if (!appId) return;
if (isLocalStatusComplete(appId)) return;
if (profileConfirmQueue.includes(appId)) return;
profileConfirmQueue.push(appId);
changed = true;
});
if (changed) {
saveProfileConfirmDataSoon();
if (force || profileConfirmAutoScan) scheduleProfileConfirmScan(200, force);
}
}
function legacyProfileStatusOnlyCheck(appId) {
if (isBannedApp(appId)) return Promise.resolve('banned');
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://store.steampowered.com/app/${appId}?cc=cn&l=schinese`,
headers: { Cookie: 'wants_mature_content=1;birthtime=0;lastagecheckage=1-0-1990;' },
timeout: PROFILE_CONFIRM_TIMEOUT,
onload(resp) {
const html = resp.responseText || '';
if (html.includes('个人资料功能受限') || html.includes('Profile Features Limited')) {
resolve('restricted');
} else if (html.includes('Steam 正在了解') || html.includes('Steam Is Learning About')) {
resolve('learning');
} else {
resolve('normal');
}
},
onerror() { resolve(null); },
ontimeout() { resolve(null); },
});
});
}
function getStoreHtmlProfileStatus(html) {
if (html.includes('个人资料功能受限') || html.includes('Profile Features Limited')) return 'restricted';
if (html.includes('Steam 正在了解') || html.includes('Steam Is Learning About')) return 'learning';
return 'normal';
}
function getStoreHtmlAppType(html) {
if (html.includes('game_area_dlc_bubble') || html.includes('This content requires') || html.includes('Downloadable Content')) return 'dlc';
if (html.includes('category1=990') || html.includes('category1=994') || html.includes('All Soundtracks') || html.includes('All Software')) return 'non_game';
return 'game';
}
function getStoreAppTypeByApi(appId) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://store.steampowered.com/api/appdetails?appids=${appId}&filters=basic&cc=cn&l=english`,
responseType: 'json',
timeout: PROFILE_CONFIRM_TIMEOUT,
onload(resp) {
try {
const data = typeof resp.response === 'object' && resp.response
? resp.response
: JSON.parse(resp.responseText || '{}');
const appData = data[String(appId)] && data[String(appId)].data;
const type = appData && appData.type;
if (type === 'dlc') resolve('dlc');
else if (type === 'game') resolve('game');
else resolve('non_game');
} catch (e) {
resolve(null);
}
},
onerror() { resolve(null); },
ontimeout() { resolve(null); },
});
});
}
async function checkLocalAppStatus(appId) {
if (isBannedApp(appId)) return { status: 'banned', appType: getLocalAppType(appId) || 'game' };
const appTypePromise = getStoreAppTypeByApi(appId);
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://store.steampowered.com/app/${appId}?cc=cn&l=schinese`,
headers: { Cookie: 'wants_mature_content=1;birthtime=0;lastagecheckage=1-0-1990;' },
timeout: PROFILE_CONFIRM_TIMEOUT,
async onload(resp) {
const html = resp.responseText || '';
const appType = await appTypePromise;
resolve({
status: getStoreHtmlProfileStatus(html),
appType: appType || getStoreHtmlAppType(html)
});
},
onerror() { resolve(null); },
ontimeout() { resolve(null); },
});
});
}
function setProfileConfirmResult(appId, result) {
appId = Number(appId);
if (!appId || !result) return;
const status = typeof result === 'string' ? result : result.status;
if (!isValidProfileConfirmStatus(status)) return;
const old = profileConfirmDB[String(appId)] || {};
const appType = typeof result === 'object' && isValidLocalAppType(result.appType)
? result.appType
: (isValidLocalAppType(old.appType) ? old.appType : 'game');
profileConfirmDB[String(appId)] = {
...old,
status,
appType,
checkedAt: Date.now()
};
}
function upsertProfileConfirmResult(appId, result) {
appId = Number(appId);
if (!appId || !result) return;
setProfileConfirmResult(appId, result);
profileConfirmQueue = profileConfirmQueue.filter(id => Number(id) !== appId);
saveProfileConfirmDataSoon();
}
function scheduleProfileConfirmScan(delay = 2000, force = false) {
if ((!force && !profileConfirmAutoScan) || profileConfirmQueue.length === 0 || profileConfirmScanning || profileConfirmScanTimer) return;
profileConfirmScanTimer = setTimeout(() => {
profileConfirmScanTimer = null;
runProfileConfirmScan();
}, delay);
}
async function runProfileConfirmScan(limit = 3000) {
if (profileConfirmScanning) return;
profileConfirmScanning = true;
profileConfirmAbort = false;
updateProfileConfirmStats();
let checked = 0;
let attempted = 0;
while (!profileConfirmAbort && profileConfirmQueue.length > 0 && attempted < limit) {
const batch = [];
while (!profileConfirmAbort && profileConfirmQueue.length > 0 && batch.length < PROFILE_CONFIRM_CONCURRENT && attempted + batch.length < limit) {
const appId = Number(profileConfirmQueue.shift());
if (!appId || isOwnedApp(appId) || isNoGameApp(appId) || isLocalStatusComplete(appId)) {
continue;
}
batch.push(appId);
}
if (batch.length === 0) {
break;
}
attempted += batch.length;
const results = await Promise.all(batch.map(async (appId) => ({
appId,
result: await checkLocalAppStatus(appId)
})));
let failedCount = 0;
let hasSavedResult = false;
results.forEach(({ appId, result }) => {
if (result && result.status) {
setProfileConfirmResult(appId, result);
checked++;
hasSavedResult = true;
} else {
failedCount++;
if (!profileConfirmQueue.includes(appId) && !isLocalStatusComplete(appId)) {
profileConfirmQueue.push(appId);
}
}
});
if (hasSavedResult) {
saveProfileConfirmDataSoon();
}
updateProfileConfirmStats();
if (failedCount === batch.length) {
break;
}
if (PROFILE_CONFIRM_BATCH_DELAY > 0 && profileConfirmQueue.length > 0 && attempted < limit) {
await new Promise(resolve => setTimeout(resolve, PROFILE_CONFIRM_BATCH_DELAY));
}
}
const wasAborted = profileConfirmAbort;
profileConfirmScanning = false;
profileConfirmAbort = false;
saveProfileConfirmDataNow();
queueAllCdkeyGameCheckers();
if (noRestrictedGames || scanHideAllGames) checkAndAutoSkip();
if (!wasAborted && profileConfirmAutoScan && profileConfirmQueue.length > 0) {
scheduleProfileConfirmScan(300);
}
}
function abortProfileConfirmScan() {
profileConfirmAbort = true;
if (profileConfirmScanTimer) {
clearTimeout(profileConfirmScanTimer);
profileConfirmScanTimer = null;
}
updateProfileConfirmStats();
}
function clearProfileConfirmData() {
profileConfirmDB = {};
profileConfirmQueue = [];
saveProfileConfirmDataNow();
}
function queueCdkeyGameChecker(element) {
if (!element || !element.classList || !element.classList.contains('cdkGameIcon')) return;
cdkeyCheckQueue.add(element);
if (cdkeyCheckTimer) return;
const run = window.requestIdleCallback || window.requestAnimationFrame || ((fn) => setTimeout(fn, 16));
cdkeyCheckTimer = run(processCdkeyCheckQueue);
}
function queueAllCdkeyGameCheckers() {
document.querySelectorAll('.cdkGameIcon').forEach((element) => {
queueCdkeyGameChecker(element);
});
}
function processCdkeyCheckQueue() {
cdkeyCheckTimer = null;
const items = Array.from(cdkeyCheckQueue).filter((element) => element && element.isConnected);
cdkeyCheckQueue.clear();
const chunk = items.splice(0, 12);
chunk.forEach((element) => {
try {
cdkeyGameChecker(element);
} catch (e) {
console.error('[Better Steampy] 标记游戏卡片失败:', e);
}
});
items.forEach((element) => cdkeyCheckQueue.add(element));
if (cdkeyCheckQueue.size > 0) {
const run = window.requestIdleCallback || window.requestAnimationFrame || ((fn) => setTimeout(fn, 16));
cdkeyCheckTimer = run(processCdkeyCheckQueue);
}
}
function fixBannedProfileConfirmResults() {
if (!delistedGamesData || !delistedGamesData.removed_apps) return 0;
let fixed = 0;
Object.keys(profileConfirmDB || {}).forEach((appid) => {
if (isBannedApp(appid) && profileConfirmDB[appid].status !== 'banned') {
profileConfirmDB[appid] = {
status: 'banned',
checkedAt: Date.now()
};
fixed++;
}
});
if (fixed > 0) {
saveProfileConfirmDataNow();
}
return fixed;
}
function exportProfileConfirmData() {
const stats = getProfileConfirmStats();
const payload = {
name: 'Better SteamPYPlus local status library',
version: PROFILE_CONFIRM_EXPORT_VERSION,
exportedAt: Date.now(),
stats: {
normal: stats.normal,
restricted: stats.restricted,
learning: stats.learning,
total: stats.normal + stats.restricted + stats.learning
},
db: profileConfirmDB
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const date = new Date().toISOString().slice(0, 10);
const a = document.createElement('a');
a.href = url;
a.download = `steampy_local_status_${date}_${payload.stats.total}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function getProfileConfirmImportDB(data) {
if (!data || typeof data !== 'object') return null;
if (data.db && typeof data.db === 'object') return data.db;
if (data.ProfileConfirmDB && typeof data.ProfileConfirmDB === 'object') return data.ProfileConfirmDB;
return data;
}
function importProfileConfirmData(data) {
const importDB = getProfileConfirmImportDB(data);
if (!importDB || typeof importDB !== 'object') {
throw new Error('invalid import data');
}
let imported = 0;
let skipped = 0;
let updated = 0;
Object.entries(importDB).forEach(([appid, value]) => {
const id = Number(appid);
if (!id || isOwnedApp(id)) {
skipped++;
return;
}
const status = typeof value === 'string' ? value : value && value.status;
if (!isValidProfileConfirmStatus(status)) {
skipped++;
return;
}
const appType = value && isValidLocalAppType(value.appType) ? value.appType : 'game';
const checkedAt = Number((value && value.checkedAt) || Date.now());
const key = String(id);
const old = profileConfirmDB[key];
if (old && old.checkedAt && Number(old.checkedAt) > checkedAt) {
skipped++;
return;
}
profileConfirmDB[key] = { status, appType, checkedAt };
if (old) updated++;
else imported++;
});
normalizeProfileConfirmQueue();
fixBannedProfileConfirmResults();
saveProfileConfirmDataNow();
return { imported, updated, skipped };
}
function importProfileConfirmDataFromFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json,.json';
input.onchange = () => {
const file = input.files && input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
const result = importProfileConfirmData(data);
iview.Notice.success({
title: 'Better Steampy',
desc: `导入完成:新增 ${result.imported},更新 ${result.updated},跳过 ${result.skipped}`,
duration: 5
});
queueAllCdkeyGameCheckers();
if (noRestrictedGames || scanHideAllGames) checkAndAutoSkip();
} catch (e) {
console.error('[Better Steampy] 导入本地状态库失败:', e);
iview.Notice.error({
title: 'Better Steampy',
desc: '导入失败,请确认文件是有效的本地状态库 JSON',
duration: 5
});
}
};
reader.readAsText(file);
};
input.click();
}
// 获取 Vue 分页父组件及翻页方法
function getPageComponent() {
try {
const pageComp = document.querySelector('.ivu-page').__vue__;
for (let i = 1; i <= 6; i++) {
let comp = pageComp;
for (let j = 0; j < i; j++) {
comp = comp.$parent;
if (!comp) break;
}
if (comp && (comp.pageHandler || comp.changePage)) {
return comp;
}
}
} catch (e) {}
return null;
}
// 获取当前页码和总页数
function getPageInfo() {
const activePageEl = document.querySelector('.zpagenav .page-ul li.active');
let currentPage = 1, maxPage = 1;
if (activePageEl) {
currentPage = parseInt(activePageEl.textContent) || 1;
const allPageNums = Array.from(document.querySelectorAll('.zpagenav .page-ul li'))
.map(li => parseInt(li.textContent))
.filter(num => !isNaN(num));
maxPage = Math.max(...allPageNums, 1);
}
return { currentPage, maxPage };
}
// 跳转到指定页码
function jumpToPage(pageNum) {
const comp = getPageComponent();
if (comp) {
if (comp.pageHandler) {
comp.pageHandler(pageNum);
} else if (comp.changePage) {
comp.changePage(pageNum);
}
return true;
}
// fallback: 点击按钮
const nextButton = document.querySelector('.ivu-page-next:not(.ivu-page-disabled)');
if (nextButton) { nextButton.click(); return true; }
return false;
}
// 从当前 DOM 中提取所有游戏的 appId
function extractCurrentPageAppIds() {
const icons = document.querySelectorAll('.cdkGameIcon');
return Array.from(icons).map(icon => {
const src = icon.getAttribute('data-src') || '';
const match = src.match(/\/apps\/(\d+)\//);
return match ? Number(match[1]) : 0;
}).filter(id => id > 0);
}
// 生成当前页数据的指纹
function getPageDataFingerprint() {
const icons = document.querySelectorAll('.cdkGameIcon');
return Array.from(icons).map(icon => icon.getAttribute('data-src') || '').join('|');
}
// 事件驱动等待:用 MutationObserver 监听 data-src 变化,比轮询快得多
function waitForPageData(expectedPage, prevFingerprint, timeout = 8000) {
return new Promise((resolve) => {
let settled = false;
let timeoutId = null;
let observer = null;
const cleanup = () => {
if (settled) return;
settled = true;
if (observer) observer.disconnect();
if (timeoutId) clearTimeout(timeoutId);
};
const tryResolve = () => {
if (settled) return;
if (fastScanAbort) { cleanup(); resolve(false); return; }
const { currentPage } = getPageInfo();
const fp = getPageDataFingerprint();
const icons = document.querySelectorAll('.cdkGameIcon');
if (currentPage === expectedPage && fp !== prevFingerprint && fp.length > 0 && icons.length > 0) {
cleanup();
resolve(true);
}
};
// 超时保底
timeoutId = setTimeout(() => {
if (!settled) {
console.log('[Better Steampy] 第', expectedPage, '页等待超时');
cleanup();
resolve(false);
}
}, timeout);
// 先立即检查(可能数据已经到了)
tryResolve();
if (settled) return;
// MutationObserver 监听 data-src 变化——Vue 更新 DOM 时立即触发
observer = new MutationObserver(() => { tryResolve(); });
const container = document.querySelector('.cdkGameIcon')?.closest('.gameblock')?.parentElement?.parentElement || document.body;
observer.observe(container, { attributes: true, attributeFilter: ['data-src'], subtree: true, childList: true });
});
}
// 创建/获取扫描进度浮窗(单例,固定位置,只更新文字)
function getScanOverlay() {
let el = document.getElementById('bsp-scan-overlay');
if (!el) {
el = document.createElement('div');
el.id = 'bsp-scan-overlay';
el.style.cssText = 'position:fixed;top:16px;right:16px;z-index:99999;background:rgba(0,0,0,0.78);color:#fff;padding:12px 22px;border-radius:8px;font-size:14px;pointer-events:auto;display:none;box-shadow:0 2px 12px rgba(0,0,0,0.3);max-width:320px;line-height:1.6;';
// 停止按钮
const btn = document.createElement('span');
btn.id = 'bsp-scan-stop';
btn.textContent = ' ✕ 停止';
btn.style.cssText = 'cursor:pointer;margin-left:10px;color:#ff6b6b;font-weight:bold;';
btn.onclick = () => { fastScanAbort = true; };
el.appendChild(document.createElement('span')); // 文字容器
el.appendChild(btn);
document.body.appendChild(el);
}
return el;
}
function showScanProgress(text) {
const el = getScanOverlay();
el.style.display = 'block';
el.querySelector('span:first-child').textContent = text;
}
function hideScanOverlay() {
const el = document.getElementById('bsp-scan-overlay');
if (el) el.style.display = 'none';
}
// 尝试从 Vue 组件直接读取游戏列表数据(比 DOM 快,不需要等渲染)
function tryGetVueGameList() {
try {
const comp = getPageComponent();
if (!comp) return null;
// 遍历组件及其子组件,查找包含游戏列表的 data
const searchData = (vm) => {
if (!vm) return null;
const d = vm.$data || vm._data || {};
for (const key of Object.keys(d)) {
const val = d[key];
if (Array.isArray(val) && val.length > 0 && val[0] && (val[0].appid || val[0].appId || val[0].app_id || val[0].icon)) {
return val;
}
}
// 搜索子组件
if (vm.$children) {
for (const child of vm.$children) {
const result = searchData(child);
if (result) return result;
}
}
return null;
};
// 从分页组件向上找到根列表组件
let target = comp;
for (let i = 0; i < 3; i++) {
const result = searchData(target);
if (result) return result;
if (target.$parent) target = target.$parent;
else break;
}
} catch (e) {}
return null;
}
// 从 Vue 数据中提取 appId
function extractAppIdsFromVueData(list) {
return list.map(item => {
// 尝试各种可能的字段名
const id = item.appid || item.appId || item.app_id || 0;
if (id) return Number(id);
// 从 icon URL 提取
const icon = item.icon || item.img || '';
const match = icon.match(/\/apps\/(\d+)\//);
return match ? Number(match[1]) : 0;
}).filter(id => id > 0);
}
// 快速扫描:跳过不符合条件的页面,直接定位到有结果的页面
async function fastScanPages(startPage, maxPage) {
if (isFastScanning) return;
isFastScanning = true;
fastScanAbort = false;
showScanProgress(`快速扫描中... ${startPage} / ${maxPage}`);
let foundPage = -1;
for (let page = startPage; page <= maxPage; page++) {
if (fastScanAbort) {
console.log('[Better Steampy] 快速扫描被中止');
break;
}
showScanProgress(`快速扫描中... ${page} / ${maxPage}`);
// 记录跳转前的数据指纹
const prevFingerprint = getPageDataFingerprint();
jumpToPage(page);
// 等待数据更新
const loaded = await waitForPageData(page, prevFingerprint, 8000);
if (!loaded) {
console.log('[Better Steampy] 第', page, '页加载超时,跳过');
continue;
}
// 优先从 Vue 数据读取(更快更可靠),fallback 到 DOM
let appIds;
const vueList = tryGetVueGameList();
if (vueList) {
appIds = extractAppIdsFromVueData(vueList);
}
if (!appIds || appIds.length === 0) {
appIds = extractCurrentPageAppIds();
}
rememberProfileConfirmCandidates(appIds);
const hasMatch = appIds.some(id => doesAppPassFilter(id));
if (hasMatch) {
foundPage = page;
console.log('[Better Steampy] 在第', page, '页找到符合条件的游戏');
break;
} else {
console.log('[Better Steampy] 第', page, '页无符合条件的游戏,继续扫描');
}
}
hideScanOverlay();
if (foundPage > 0) {
queueAllCdkeyGameCheckers();
iview.Notice.success({
title: 'Better Steampy',
desc: `已跳转到第 ${foundPage} 页(跳过了 ${foundPage - startPage} 页无结果页面)`,
duration: 3
});
} else if (!fastScanAbort) {
iview.Notice.warning({
title: 'Better Steampy',
desc: `第 ${startPage} ~ ${maxPage} 页均无符合条件的游戏`,
duration: 4
});
if (startPage > 1) {
jumpToPage(startPage);
}
}
isFastScanning = false;
isAutoSkipping = false;
autoSkipTargetPage = 0;
}
// 检测当前页是否为空,如果为空则启动快速扫描
function checkAndAutoSkip() {
if (autoSkipTimer) clearTimeout(autoSkipTimer);
if (isFastScanning) return;
autoSkipTimer = setTimeout(() => {
const cdkGameIcons = document.querySelectorAll('.cdkGameIcon');
const allGames = Array.from(cdkGameIcons).map(icon => icon.closest('.gameblock')).filter(el => el !== null);
const visibleGames = allGames.filter(el => el.style.display !== 'none');
// 获取当前页面游戏的ID列表
const currentGameIds = Array.from(cdkGameIcons).map(icon => {
const src = icon.getAttribute('data-src') || '';
const match = src.match(/\/apps\/(\d+)\//);
return match ? match[1] : '';
}).join(',');
const dataChanged = currentGameIds !== lastGameIds && lastGameIds !== '';
if (isAutoSkipping) {
if (!dataChanged) return;
isAutoSkipping = false;
} else if (dataChanged) {
autoSkipTargetPage = 0;
}
lastGameIds = currentGameIds;
// 当前页有游戏但全部被隐藏
if (allGames.length > 0 && visibleGames.length === 0) {
const { currentPage, maxPage } = getPageInfo();
if (currentPage >= maxPage) {
iview.Notice.warning({
title: 'Better Steampy',
desc: '当前页无符合条件的游戏,已到最后一页',
duration: 3
});
return;
}
// 启动快速扫描,从下一页开始
fastScanPages(currentPage + 1, maxPage);
} else {
isAutoSkipping = false;
autoSkipTargetPage = 0;
}
}, 500);
}
// 重置自动跳页状态(用于手动翻页时调用)
function resetAutoSkipState() {
isAutoSkipping = false;
autoSkipTargetPage = 0;
// 如果正在快速扫描,中止它
if (isFastScanning) {
fastScanAbort = true;
}
}
//读取个人库存及愿望单并储存
function getOwnAndWish() {
return new Promise((resolve, reject) => {
var wishlist = [];
var ownedApps = [];
GM_xmlhttpRequest({
method: 'GET',
url:
'https://store.steampowered.com/dynamicstore/userdata/?t=' +
Math.trunc(Date.now() / 1000),
responseType: 'json',
onload: function (response) {
let data = JSON.parse(response.responseText);
wishlist = data.rgWishlist;
ownedApps = data.rgOwnedApps;
let previousSaves = GM_getValue('Saves');
let newSave = {
wishlist: wishlist,
ownedApps: ownedApps,
familygameList: previousSaves.familygameList,
lastupdatetime: new Date().getTime(),
};
GM_setValue('Saves', newSave);
Saves = newSave;
saveProfileConfirmDataNow();
iview.Notice.success({
title: `Better Steampy`,
desc: `已加载 ${ownedApps.length} 个库存游戏及DLC,${wishlist.length} 个愿望单游戏`,
});
resolve(newSave);
},
});
});
}
//读取家庭库并储存
function getFamilyGame() {
return new Promise((resolve, reject) => {
var access_token;
var family_groupid;
var familygameList = [];
GM_xmlhttpRequest({
method: 'GET',
url: 'https://store.steampowered.com/pointssummary/ajaxgetasyncconfig',
responseType: 'json',
onload: function (response) {
let data = JSON.parse(response.responseText);
access_token = data.data.webapi_token; // access_token
GM_xmlhttpRequest({
method: 'GET',
url: `https://api.steampowered.com/IFamilyGroupsService/GetFamilyGroupForUser/v1/?access_token=${access_token}`,
responseType: 'json',
onload: function (response) {
let data = JSON.parse(response.responseText);
family_groupid = data.response.family_groupid; // family_groupid
GM_xmlhttpRequest({
method: 'GET',
url: `https://api.steampowered.com/IFamilyGroupsService/GetSharedLibraryApps/v1/?access_token=${access_token}&family_groupid=${family_groupid}&include_own=true`,
responseType: 'json',
onload: function (response) {
let data = JSON.parse(response.responseText);
data.response.apps.forEach((app) => {
if (app.exclude_reason == 0) {
familygameList.push(app.appid);
}
});
let previousSaves = GM_getValue('Saves');
let newSave = {
wishlist: previousSaves.wishlist,
ownedApps: previousSaves.ownedApps,
familygameList: familygameList,
lastupdatetime: new Date().getTime(),
};
GM_setValue('Saves', newSave);
Saves = newSave;
iview.Notice.success({
title: `Better Steampy`,
desc: `已加载 ${familygameList.length} 个家庭库游戏`,
});
resolve(familygameList);
},
});
},
});
},
});
});
}
// Rebuild the local +1 library from the user's own confirmed profile-status DB.
function getLimitedGamesList() {
return new Promise((resolve) => {
const appIds = typeof extractCurrentPageAppIds === 'function' ? extractCurrentPageAppIds() : [];
rememberProfileConfirmCandidates(appIds);
const localNormalApps = rebuildLocalLimitedApps();
iview.Notice.success({
title: `Better Steampy`,
desc: `已收集当前页 App,待扫描 ${profileConfirmQueue.length} 个;本地可加一 ${localNormalApps.length} 个`,
});
resolve(localNormalApps);
});
}
//获取非游戏列表
function getNogameList() {
return new Promise((resolve) => {
const localNoGameApps = rebuildLocalNoGameList();
iview.Notice.success({
title: `Better Steampy`,
desc: `已从本地状态库加载 ${localNoGameApps.length} 个DLC及非游戏 App`,
});
resolve(localNoGameApps);
});
}
//获取下架游戏列表(尝试从 SWI 插件缓存读取)
function getDelistedGamesList() {
return new Promise((resolve, reject) => {
console.log('[Better Steampy] 开始获取下架游戏数据...');
// 尝试从 chrome.storage 读取 SWI 插件的缓存数据
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
chrome.storage.local.get(['swi_decommissioned'], function(result) {
if (result.swi_decommissioned && result.swi_decommissioned.removed_apps) {
console.log('[Better Steampy] 从 SWI 插件缓存读取下架游戏数据');
delistedGamesData = result.swi_decommissioned;
GM_setValue('DelistedGamesData', delistedGamesData);
fixBannedProfileConfirmResults();
const count = Object.keys(delistedGamesData.removed_apps).length;
console.log('[Better Steampy] 成功加载下架游戏数据,共', count, '个');
iview.Notice.success({
title: `Better Steampy`,
desc: `已从 SWI 插件加载 ${count} 个下架游戏数据`,
});
resolve(delistedGamesData);
} else {
// SWI 插件没有缓存,尝试从 API 获取
fetchFromAPI();
}
});
} else {
// 不支持 chrome.storage,直接从 API 获取
fetchFromAPI();
}
function fetchFromAPI() {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://steam-tracker.com/api?action=GetAppListV3',
responseType: 'json',
timeout: 10000,
onload: function (response) {
try {
console.log('[Better Steampy] API 响应状态:', response.status);
var data = JSON.parse(response.responseText);
console.log('[Better Steampy] 解析后的数据结构:', Object.keys(data));
// 转换数组格式为对象格式,方便查询
if (data.removed_apps && Array.isArray(data.removed_apps)) {
const removed_apps_obj = {};
data.removed_apps.forEach(app => {
removed_apps_obj[app.appid] = {
name: app.name,
category: app.category,
type: app.type
};
});
const convertedData = {
removed_apps: removed_apps_obj
};
GM_setValue('DelistedGamesData', convertedData);
delistedGamesData = convertedData;
fixBannedProfileConfirmResults();
const count = Object.keys(removed_apps_obj).length;
console.log('[Better Steampy] 成功加载下架游戏数据,共', count, '个');
iview.Notice.success({
title: `Better Steampy`,
desc: `已加载 ${count} 个下架游戏数据`,
});
resolve(convertedData);
} else {
throw new Error('数据格式不正确');
}
} catch (e) {
console.error('[Better Steampy] 解析下架游戏数据失败:', e);
iview.Notice.error({
title: `Better Steampy`,
desc: `加载下架游戏数据失败: ${e.message}`,
});
reject(e);
}
},
onerror: function(error) {
console.error('[Better Steampy] 请求下架游戏数据失败:', error);
iview.Notice.error({
title: `Better Steampy`,
desc: `请求下架游戏数据失败,请检查网络连接`,
});
reject(error);
},
ontimeout: function() {
console.error('[Better Steampy] 请求下架游戏数据超时');
iview.Notice.error({
title: `Better Steampy`,
desc: `请求下架游戏数据超时`,
});
reject(new Error('timeout'));
}
});
}
});
}
//获取慈善包数据(从 barter.vg)
function getBundledGamesList() {
return new Promise((resolve, reject) => {
console.log('[Better Steampy] 开始获取慈善包数据...');
GM_xmlhttpRequest({
method: 'GET',
url: 'https://bartervg.com/browse/bundles/json/',
responseType: 'json',
timeout: 30000,
onload: function (response) {
try {
var data = typeof response.response === 'object' ? response.response : JSON.parse(response.responseText);
// data 格式: { "appid": { bundles: N }, ... }
var bundleMap = {};
var count = 0;
for (var sku in data) {
if (data[sku] && data[sku].bundles && data[sku].bundles > 0) {
bundleMap[sku] = data[sku].bundles;
count++;
}
}
bundledAppsData = bundleMap;
GM_setValue('BundledAppsData', bundleMap);
GM_setValue('BundledAppsDataTime', Date.now());
console.log('[Better Steampy] 成功加载慈善包数据,共', count, '个进过包的游戏');
iview.Notice.success({
title: 'Better Steampy',
desc: `已加载 ${count} 个进过慈善包的游戏数据`,
});
resolve(bundleMap);
} catch (e) {
console.error('[Better Steampy] 解析慈善包数据失败:', e);
reject(e);
}
},
onerror: function(error) {
console.error('[Better Steampy] 请求慈善包数据失败:', error);
reject(error);
},
ontimeout: function() {
console.error('[Better Steampy] 请求慈善包数据超时');
reject(new Error('timeout'));
}
});
});
}
//初始化脚本配置菜单
function init() {
const settings = document.createElement('div');
settings.innerHTML = `
<div id="settings" class="ml-20-rem">
<div class="withdraw" @click="modal=true, updateValues()">脚本设置</div>
<Modal v-model="modal" width="980" class-name="bsp-settings-modal" footer-hide :styles="modalStyles" transition-names="">
<div class="bsp-settings-shell">
<div class="bsp-settings-head">
<div>
<h2>Better SteamPYPlus</h2>
<p>本地库、标记和页面优化集中管理</p>
</div>
<div class="bsp-head-actions">
<i-Button type="primary" size="small" @click="reloadSaves" :loading="refershSaves_loading">同步存档</i-Button>
<i-Button size="small" @click="clearSaves">清除存档</i-Button>
</div>
</div>
<div class="bsp-settings-grid">
<section class="bsp-panel bsp-owned-panel">
<div class="bsp-panel-title">
<h3>拥有状态</h3>
<span>24h 自动更新</span>
</div>
<div class="bsp-last-update">上次更新:<i-time :time="lastUpdateTime" :interval="1"></i-time></div>
<div class="bsp-mini-stats">
<div><span>库存+DLC</span><strong>{{ownedApps}}</strong></div>
<div><span>愿望单</span><strong>{{wishlist}}</strong></div>
<div><span>家庭库</span><strong>{{familygameList}}</strong></div>
</div>
<label class="bsp-switch-line">
<span class="bsp-switch-label">已加入家庭组</span>
<i-Switch v-model="isInFamilyGroup" @on-change="isInFamilyGroup_change" />
</label>
<p class="bsp-muted">暂不支持捆绑包标记</p>
</section>
<section class="bsp-panel bsp-profile-panel">
<div class="bsp-panel-title">
<h3>受限标记</h3>
<span>本地自维护</span>
</div>
<p class="bsp-muted">加一判断只使用本地确认库;自动扫描开启后,新 App 会后台抓取 Steam 商店页确认。</p>
<div class="bsp-profile-controls">
<label class="bsp-switch-line">
<span class="bsp-switch-label">受限游戏标注</span>
<i-Switch v-model="checkIsProfileFeatureLimited" @on-change="checkIsProfileFeatureLimited_change" />
</label>
<label class="bsp-switch-line">
<span class="bsp-switch-label">自动扫描</span>
<i-Switch v-model="profileConfirmAutoScan" @on-change="profileConfirmAutoScan_change" />
<em :class="profileConfirmScanning ? 'bsp-state bsp-state-running' : 'bsp-state'">{{profileConfirmScanning ? '扫描中' : '空闲'}}</em>
</label>
</div>
<div class="bsp-profile-stats">
<div class="bsp-stat"><span>本地库</span><strong>{{profileConfirmTotal}}</strong></div>
<div class="bsp-stat bsp-stat-ok"><span>可加一</span><strong>{{profileConfirmAddable}}</strong></div>
<div class="bsp-stat bsp-stat-dlc"><span>DLC</span><strong>{{profileConfirmDlc}}</strong></div>
<div class="bsp-stat bsp-stat-nongame"><span>非游戏</span><strong>{{profileConfirmNonGame}}</strong></div>
<div class="bsp-stat bsp-stat-bad"><span>受限</span><strong>{{profileConfirmRestricted}}</strong></div>
<div class="bsp-stat bsp-stat-warn"><span>了解中</span><strong>{{profileConfirmLearning}}</strong></div>
<div class="bsp-stat bsp-stat-banned"><span>封禁</span><strong>{{profileConfirmBanned}}</strong></div>
<div class="bsp-stat bsp-stat-pending"><span>待扫描</span><strong>{{profileConfirmQueued}}</strong></div>
</div>
<div class="bsp-profile-actions">
<i-Button type="primary" size="small" @click="startProfileConfirmScan" :loading="profileConfirmScanning">开始扫描</i-Button>
<i-Button size="small" @click="stopProfileConfirmScan">停止</i-Button>
<i-Button size="small" @click="reloadLimitedSaves" :loading="reloadLimitedSaves_loading">收集当前页</i-Button>
<i-Button size="small" @click="exportProfileConfirmSaves">导出</i-Button>
<i-Button size="small" @click="importProfileConfirmSaves">导入</i-Button>
<i-Button type="error" ghost size="small" @click="clearProfileConfirmSaves">清空</i-Button>
</div>
</section>
<section class="bsp-panel bsp-color-panel">
<div class="bsp-panel-title"><h3>标记颜色</h3></div>
<div class="bsp-color-grid">
<label>已拥有 <Color-Picker v-model="ownedAppsColor" size="small" :colors="defaultcolors" @on-change="ownedAppsColor_change" /></label>
<label>愿望单 <Color-Picker v-model="wishlistColor" size="small" :colors="defaultcolors" @on-change="wishlistColor_change" /></label>
<label>家庭库 <Color-Picker v-model="familygameColor" size="small" :colors="defaultcolors" @on-change="familygameColor_change" /></label>
<label>未拥有 <Color-Picker v-model="unownedColor" size="small" :colors="defaultcolors" @on-change="unownedColor_change" /></label>
</div>
</section>
<section class="bsp-panel bsp-page-panel">
<div class="bsp-panel-title"><h3>网页优化</h3></div>
<label class="bsp-switch-line">
<span class="bsp-switch-label">隐藏右下推广侧栏</span>
<i-Switch v-model="isSuspensionOff" @on-change="isSuspensionOff_change" />
</label>
</section>
<section class="bsp-panel bsp-blacklist-panel">
<div class="bsp-panel-title">
<h3>黑名单</h3>
<span>{{blacklistCount}} 个</span>
</div>
<p class="bsp-muted">点击游戏卡片上的禁用按钮即可加入黑名单。</p>
<div v-if="blacklistItems.length > 0" class="bsp-blacklist-list">
<div v-for="item in blacklistItems" :key="item.appId" class="bsp-blacklist-item">
<span>
<a :href="'https://store.steampowered.com/app/' + item.appId" target="_blank">{{item.name}}</a>
<em>{{item.appId}}</em>
</span>
<i-Button size="small" type="error" ghost @click="removeBlacklistItem(item.appId)">移除</i-Button>
</div>
</div>
<div v-else class="bsp-empty">黑名单为空</div>
<i-Button type="error" ghost size="small" @click="clearBlacklist" v-if="blacklistItems.length > 0">清空黑名单</i-Button>
</section>
</div>
</div>
</Modal>
</div>
`;
const filter = document.createElement('div');
filter.innerHTML = `
<div id="filter" class="ml-20-rem">
<div id="filter" style="position: relative; display: flex; align-items: center; flex-wrap: wrap; gap: 6px;">
<span style="color:#515a6e;font-size:13px;margin-right:4px;font-weight:500;">筛选:</span>
<Checkbox-Group v-model="filter" @on-change="filterChange" style="display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;">
<Checkbox label="noOwnedGames"><Tag :color="filter.includes('noOwnedGames')?'primary':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏已拥有</Tag></Checkbox>
<Checkbox label="noRestrictedGames"><Tag :color="filter.includes('noRestrictedGames')?'primary':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏受限</Tag></Checkbox>
<Checkbox label="noDlc"><Tag :color="filter.includes('noDlc')?'primary':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏DLC</Tag></Checkbox>
<Checkbox label="noBlacklistGames"><Tag :color="filter.includes('noBlacklistGames')?'primary':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏黑名单</Tag></Checkbox>
<Checkbox label="noBundledGames"><Tag :color="filter.includes('noBundledGames')?'success':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏慈善包</Tag></Checkbox>
<Checkbox label="scanHideAllGames"><Tag :color="filter.includes('scanHideAllGames')?'error':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">全部隐藏</Tag></Checkbox>
<div style="display: inline-flex; align-items: center; position: relative;">
<Checkbox label="onlyDelistedGames"><Tag :color="filter.includes('onlyDelistedGames')?'warning':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">仅下架</Tag></Checkbox>
<span
v-if="showDelistedTypes"
@click.stop.prevent="toggleDelistedTypesPanel"
style="cursor:pointer;padding:0 4px;transition:transform 0.2s;display:inline-block;user-select:none;font-size:10px;color:#808695;"
:style="{ transform: delistedTypesPanelExpanded ? 'rotate(90deg)' : 'rotate(0deg)' }">▼</span>
<div v-if="showDelistedTypes && delistedTypesPanelExpanded"
style="position:absolute;left:100%;top:-4px;margin-left:6px;background:#fff;border:1px solid #dcdee2;border-radius:4px;padding:6px 10px;box-shadow:0 2px 8px rgba(0,0,0,0.15);z-index:1000;min-width:160px;">
<Checkbox-Group v-model="delistedTypes" @on-change="delistedTypesChange" style="display:flex;flex-direction:column;gap:4px;font-size:12px;">
<Checkbox label="purchase disabled" style="margin:0;">Purchase Disabled</Checkbox>
<Checkbox label="delisted" style="margin:0;">Delisted</Checkbox>
<Checkbox label="f2p" style="margin:0;">F2P</Checkbox>
<Checkbox label="unreleased" style="margin:0;">Unreleased</Checkbox>
<Checkbox label="test app" style="margin:0;">Test App</Checkbox>
<Checkbox label="retail only" style="margin:0;">Retail Only</Checkbox>
<Checkbox label="pre-order exclusive" style="margin:0;">Pre-order</Checkbox>
<Checkbox label="banned" style="margin:0;">Banned</Checkbox>
</Checkbox-Group>
</div>
</div>
</Checkbox-Group>
`;
const targetElement = document.querySelector('.balanceTitle > div');
targetElement.appendChild(settings);
targetElement.appendChild(filter);
settingsVm = new Vue({
el: '#settings',
data() {
return {
reloadLimitedSaves_loading: false,
refershSaves_loading: false,
modal: false,
modalStyles: {
top: '50%',
transform: 'translateY(-50%)'
},
lastUpdateTime: Saves.lastupdatetime,
ownedApps: Saves.ownedApps.length,
wishlist: Saves.wishlist.length,
familygameList: Saves.familygameList.length,
limitedApps: limitedApps.length,
profileConfirmTotal: getProfileConfirmStats().total,
profileConfirmAddable: limitedApps.length,
profileConfirmNormal: getProfileConfirmStats().normal,
profileConfirmRestricted: getProfileConfirmStats().restricted,
profileConfirmLearning: getProfileConfirmStats().learning,
profileConfirmBanned: getProfileConfirmStats().banned,
profileConfirmDlc: getProfileConfirmStats().dlc,
profileConfirmNonGame: getProfileConfirmStats().nonGame,
profileConfirmUnknown: getProfileConfirmStats().unknown,
profileConfirmQueued: getProfileConfirmStats().queued,
profileConfirmScanning: profileConfirmScanning,
profileConfirmAutoScan: profileConfirmAutoScan,
isInFamilyGroup: JSON.parse(localStorage.getItem('isInfamily')),
checkIsProfileFeatureLimited: JSON.parse(
localStorage.getItem('IsProfileFeatureLimited')
),
isSuspensionOff: JSON.parse(localStorage.getItem('isSuspensionOff')),
ownedAppsColor: localStorage.getItem('ownedColor'),
wishlistColor: localStorage.getItem('wishlistColor'),
familygameColor: localStorage.getItem('familygameColor'),
unownedColor: localStorage.getItem('unownedColor'),
defaultcolors: ['#0c8918', '#177cb0', '#ff8936', '#ff2e63'],
blacklistItems: [...blacklist],
blacklistCount: blacklist.length,
};
},
methods: {
updateValues() {
this.ownedApps = Saves.ownedApps.length;
this.wishlist = Saves.wishlist.length;
this.familygameList = Saves.familygameList.length;
this.limitedApps = limitedApps.length;
this.profileConfirmTotal = getProfileConfirmStats().total;
this.profileConfirmAddable = limitedApps.length;
this.lastUpdateTime = Saves.lastupdatetime;
this.blacklistItems = [...blacklist];
this.blacklistCount = blacklist.length;
updateProfileConfirmStats();
},
isInFamilyGroup_change(status) {
if (status) {
localStorage.setItem('isInfamily', JSON.stringify(true));
} else {
localStorage.removeItem('isInfamily');
}
},
checkIsProfileFeatureLimited_change(status) {
if (status) {
localStorage.setItem('IsProfileFeatureLimited', JSON.stringify(true));
Saves = GM_getValue('Saves');
queueAllCdkeyGameCheckers();
checkAndAutoSkip(); // 添加自动跳页检测
} else {
localStorage.removeItem('IsProfileFeatureLimited');
const elements = document.querySelectorAll('.ProfileFeaturesLimited');
elements.forEach((element) => {
element.parentNode.removeChild(element);
});
}
},
isSuspensionOff_change(status) {
if (status) {
localStorage.setItem('isSuspensionOff', JSON.stringify(true));
GM_addStyle('.suspension{display:none}');
} else {
GM_addStyle('.suspension{display:block}');
localStorage.removeItem('isSuspensionOff');
}
},
ownedAppsColor_change(color) {
ownedColor = color;
localStorage.setItem('ownedColor', color);
queueAllCdkeyGameCheckers();
},
wishlistColor_change(color) {
wishlistColor = color;
localStorage.setItem('wishlistColor', color);
queueAllCdkeyGameCheckers();
},
familygameColor_change(color) {
familygameColor = color;
localStorage.setItem('familygameColor', color);
queueAllCdkeyGameCheckers();
},
unownedColor_change(color) {
unownedColor = color;
localStorage.setItem('unownedColor', color);
queueAllCdkeyGameCheckers();
},
async reloadSaves() {
this.$Notice.info({
title: '正在重载存档',
});
this.refershSaves_loading = true;
await Promise.all([
getOwnAndWish(),
this.isInFamilyGroup ? getFamilyGame() : Promise.resolve(),
]);
Saves = GM_getValue('Saves');
saveProfileConfirmDataNow();
queueAllCdkeyGameCheckers();
this.updateValues();
this.refershSaves_loading = false;
this.$Notice.success({
title: '重载完毕',
});
checkAndAutoSkip(); // 添加自动跳页检测
},
async reloadLimitedSaves() {
this.$Notice.info({
title: '正在收集当前页 App',
});
this.reloadLimitedSaves_loading = true;
await getLimitedGamesList();
this.updateValues();
this.reloadLimitedSaves_loading = false;
this.$Notice.success({
title: '收集完毕',
});
checkAndAutoSkip(); // 添加自动跳页检测
},
profileConfirmAutoScan_change(status) {
profileConfirmAutoScan = !!status;
GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, profileConfirmAutoScan);
updateProfileConfirmStats();
if (profileConfirmAutoScan) {
scheduleProfileConfirmScan(200);
} else {
abortProfileConfirmScan();
}
},
startProfileConfirmScan() {
profileConfirmAutoScan = true;
GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, true);
updateProfileConfirmStats();
runProfileConfirmScan(10000);
},
stopProfileConfirmScan() {
profileConfirmAutoScan = false;
GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, false);
abortProfileConfirmScan();
this.$Notice.info({ title: '已停止本地库扫描' });
},
exportProfileConfirmSaves() {
exportProfileConfirmData();
this.$Notice.success({ title: '本地状态库已导出' });
},
importProfileConfirmSaves() {
importProfileConfirmDataFromFile();
},
clearProfileConfirmSaves() {
clearProfileConfirmData();
this.$Notice.success({ title: '本地状态库已清空' });
queueAllCdkeyGameCheckers();
},
clearSaves() {
this.$Notice.info({
title: '存档已清除',
});
let nullSaves = {
wishlist: [],
ownedApps: [],
familygameList: [],
lastupdatetime: 0,
};
Saves = nullSaves;
GM_setValue('Saves', nullSaves);
this.updateValues();
},
removeBlacklistItem(appId) {
removeFromBlacklist(appId);
this.blacklistItems = [...blacklist];
this.blacklistCount = blacklist.length;
// 刷新页面显示
queueAllCdkeyGameCheckers();
},
clearBlacklist() {
blacklist = [];
GM_setValue('Blacklist', blacklist);
this.blacklistItems = [];
this.blacklistCount = 0;
queueAllCdkeyGameCheckers();
this.$Notice.success({ title: '黑名单已清空' });
},
},
});
new Vue({
el: '#filter',
data() {
return {
filter: [],
delistedTypes: ['purchase disabled', 'delisted', 'f2p', 'unreleased', 'test app', 'retail only', 'pre-order exclusive', 'banned'],
showDelistedTypes: false,
delistedTypesPanelExpanded: false,
};
},
computed: {},
methods: {
filterChange() {
noownedGames = this.filter.includes('noOwnedGames');
noRestrictedGames = this.filter.includes('noRestrictedGames');
noDlc = this.filter.includes('noDlc');
noBlacklistGames = this.filter.includes('noBlacklistGames');
noBundledGames = this.filter.includes('noBundledGames');
scanHideAllGames = this.filter.includes('scanHideAllGames');
onlyDelistedGames = this.filter.includes('onlyDelistedGames');
if (onlyDelistedGames && (!delistedGamesData || !delistedGamesData.removed_apps)) {
getDelistedGamesList().then(() => {
queueAllCdkeyGameCheckers();
checkAndAutoSkip();
}).catch(() => {});
}
if (noBundledGames && !bundledAppsData) {
getBundledGamesList().then(() => {
queueAllCdkeyGameCheckers();
checkAndAutoSkip();
}).catch(() => {});
}
this.showDelistedTypes = onlyDelistedGames;
if (onlyDelistedGames) {
this.delistedTypesPanelExpanded = true;
}
delistedTypes = this.delistedTypes;
queueAllCdkeyGameCheckers();
checkAndAutoSkip();
},
delistedTypesChange() {
delistedTypes = this.delistedTypes;
queueAllCdkeyGameCheckers();
checkAndAutoSkip();
},
toggleDelistedTypesPanel() {
this.delistedTypesPanelExpanded = !this.delistedTypesPanelExpanded;
},
},
});
if (localStorage.getItem('isSuspensionOff') === 'true') {
GM_addStyle('.suspension{display:none}');
}
}
//游戏状态标记-CDKEY
function cdkeyGameChecker(element) {
const isAppOwned = (appId) => Saves.ownedApps.includes(appId);
const isAppinwishlist = (appId) => Saves.wishlist.includes(appId);
const isAppShared = (appId) => Saves.familygameList.includes(appId);
const isNotLimited = (appId) => isProfileRestrictedByConfirmedStatus(appId);
const isDLC = (appId) => isNoGameApp(appId);
const getAppId = (url) => (url.match(/\/apps\/(\d+)\//) || [])[1] || null;
const getBundleId = (url) =>(url.match(/\/bundles\/(\d+)\//) || [])[1] || null;
const appId = Number(getAppId(element.getAttribute('data-src')));
const gameNameElement = element
.closest('.gameblock')
.querySelector('.gameName');
const gameBlock = element.closest('.gameblock');
// 检测游戏是否下架以及下架类型(使用 Steam-Tracker 数据)
const getDelistedType = () => {
if (!delistedGamesData || !delistedGamesData.removed_apps) {
return null;
}
const appIdStr = appId.toString();
const gameData = delistedGamesData.removed_apps[appIdStr];
if (!gameData) {
// 不是下架游戏
return null;
}
// Steam-Tracker 的 category 字段对应下架类型
const category = gameData.category;
if (!category) {
return null;
}
// 映射 Steam-Tracker 的 category 到我们的类型
const categoryMap = {
'Purchase disabled': 'purchase disabled',
'Delisted': 'delisted',
'Delisted video': 'delisted',
'F2P': 'f2p',
'Unreleased': 'unreleased',
'Test app': 'test app',
'Retail only': 'retail only',
'Pre-order exclusive': 'pre-order exclusive',
'Banned': 'banned'
};
const mappedType = categoryMap[category] || category.toLowerCase();
return mappedType;
};
if (appId != 0) {
rememberProfileConfirmCandidate(appId);
element.parentElement.parentElement.style.display = 'block';
// 添加黑名单按钮(如果还没有)
if (gameBlock && !gameBlock.querySelector('.bsp-blacklist-btn')) {
const btn = document.createElement('div');
btn.className = 'bsp-blacklist-btn';
btn.title = isInBlacklist(appId) ? '已在黑名单(点击移除)' : '加入黑名单';
btn.textContent = isInBlacklist(appId) ? '✅' : '🚫';
btn.style.cssText = 'position:absolute;top:4px;left:4px;z-index:100;cursor:pointer;font-size:18px;line-height:1;background:rgba(0,0,0,0.5);border-radius:50%;width:26px;height:26px;display:flex;align-items:center;justify-content:center;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseenter = () => { btn.style.opacity = '1'; };
btn.onmouseleave = () => { btn.style.opacity = '0.7'; };
btn.onclick = (e) => {
e.stopPropagation();
e.preventDefault();
const gameName = gameNameElement ? gameNameElement.textContent.trim() : '';
if (isInBlacklist(appId)) {
removeFromBlacklist(appId);
btn.textContent = '🚫';
btn.title = '加入黑名单';
iview.Notice.info({ title: 'Better Steampy', desc: `已将「${gameName}」移出黑名单`, duration: 2 });
} else {
addToBlacklist(appId, gameName);
btn.textContent = '✅';
btn.title = '已在黑名单(点击移除)';
iview.Notice.info({ title: 'Better Steampy', desc: `已将「${gameName}」加入黑名单`, duration: 2 });
}
// 如果启用了黑名单筛选,立即刷新显示
if (noBlacklistGames) {
queueAllCdkeyGameCheckers();
checkAndAutoSkip();
}
};
// 插入到 gameBlock 中(相对定位的容器)
const imgWrap = gameBlock.querySelector('.cdkGameImg') || gameBlock;
imgWrap.style.position = 'relative';
imgWrap.appendChild(btn);
} else if (gameBlock) {
// 更新已有按钮状态
const existingBtn = gameBlock.querySelector('.bsp-blacklist-btn');
if (existingBtn) {
existingBtn.textContent = isInBlacklist(appId) ? '✅' : '🚫';
existingBtn.title = isInBlacklist(appId) ? '已在黑名单(点击移除)' : '加入黑名单';
}
}
if (scanHideAllGames) {
element.parentElement.parentElement.style.display = 'none';
return;
}
// 黑名单筛选
if (noBlacklistGames && isInBlacklist(appId)) {
element.parentElement.parentElement.style.display = 'none';
return;
}
// 慈善包筛选
if (noBundledGames && bundledAppsData) {
const bundleCount = bundledAppsData[appId.toString()];
if (bundleCount && bundleCount > 0) {
element.parentElement.parentElement.style.display = 'none';
return;
}
}
// 下架游戏筛选逻辑
if (onlyDelistedGames) {
const delistedType = getDelistedType();
if (!delistedType || !delistedTypes.includes(delistedType)) {
// 不是下架游戏,或者不在选中的下架类型中,隐藏
element.parentElement.parentElement.style.display = 'none';
return;
}
}
if (noDlc) {
if (isDLC(appId)) {
element.parentElement.parentElement.style.display = 'none';
}
}
if (isAppOwned(appId)) {
if (noownedGames) {
element.parentElement.parentElement.style.display = 'none';
} else {
gameNameElement.style.color = ownedColor;
}
} else if (isAppShared(appId)) {
gameNameElement.style.color = familygameColor;
} else if (isAppinwishlist(appId)) {
gameNameElement.style.color = wishlistColor;
} else {
gameNameElement.style.color = unownedColor;
}
if (localStorage.getItem('IsProfileFeatureLimited')) {
const existingDiscountDiv = element.parentElement.querySelector(
'.ProfileFeaturesLimited'
);
if (existingDiscountDiv) {
existingDiscountDiv.remove();
}
// 恢复使用 limitedApps 列表判断(不在列表中=受限)
if (isNotLimited(appId)) {
if (noRestrictedGames) {
element.parentElement.parentElement.style.display = 'none';
} else {
const discountDiv = document.createElement('div');
discountDiv.className = 'ProfileFeaturesLimited';
discountDiv.textContent = '资料受限';
element.parentElement.appendChild(discountDiv);
}
}
}
}
}
//加载存档
function load() {
var previousSave = GM_getValue('Saves');
if (previousSave !== undefined) {
Saves = GM_getValue('Saves');
} else {
GM_setValue('Saves', Saves);
}
var previousProfileConfirmDB = GM_getValue(PROFILE_CONFIRM_DB_KEY);
if (previousProfileConfirmDB !== undefined && previousProfileConfirmDB && typeof previousProfileConfirmDB === 'object') {
profileConfirmDB = previousProfileConfirmDB;
}
rebuildLocalLimitedApps();
var previousProfileConfirmQueue = GM_getValue(PROFILE_CONFIRM_QUEUE_KEY);
if (Array.isArray(previousProfileConfirmQueue)) {
profileConfirmQueue = previousProfileConfirmQueue.map(Number).filter(id => id > 0);
normalizeProfileConfirmQueue();
}
var previousProfileConfirmAutoScan = GM_getValue(PROFILE_CONFIRM_AUTO_SCAN_KEY);
var profileConfirmAutoScanMigrated = GM_getValue(PROFILE_CONFIRM_AUTO_SCAN_MIGRATED_KEY);
if (!profileConfirmAutoScanMigrated) {
profileConfirmAutoScan = false;
GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, false);
GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_MIGRATED_KEY, true);
} else if (previousProfileConfirmAutoScan !== undefined) {
profileConfirmAutoScan = !!previousProfileConfirmAutoScan;
} else {
GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, profileConfirmAutoScan);
}
rebuildLocalNoGameList();
normalizeProfileConfirmQueue();
var previousDelistedGamesData = GM_getValue('DelistedGamesData');
if (previousDelistedGamesData !== undefined && previousDelistedGamesData !== null && previousDelistedGamesData.removed_apps) {
delistedGamesData = previousDelistedGamesData;
fixBannedProfileConfirmResults();
}
// 加载黑名单
var previousBlacklist = GM_getValue('Blacklist');
if (previousBlacklist !== undefined && Array.isArray(previousBlacklist)) {
blacklist = previousBlacklist;
console.log('[Better Steampy] 从缓存加载黑名单,共', blacklist.length, '个');
}
// 加载慈善包数据
var previousBundledAppsData = GM_getValue('BundledAppsData');
if (previousBundledAppsData !== undefined && previousBundledAppsData !== null && typeof previousBundledAppsData === 'object') {
bundledAppsData = previousBundledAppsData;
}
//自动更新
if (new Date().getTime() - Saves.lastupdatetime > 86400000) {
iview.Notice.info({
title: '存档自动更新中',
});
getOwnAndWish();
if (JSON.parse(localStorage.getItem('isInfamily'))) {
getFamilyGame();
}
rebuildLocalLimitedApps();
rebuildLocalNoGameList();
}
ensureProfileConfirmAutoScan(1000);
}
//监听页面变化
function observePageChanges() {
const config = {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-src'],
};
let hasExecuted = false;
let pageChangeTimer = null; // 用于检测页面数据加载完成
let paginationListenerAdded = false; // 是否已添加分页监听
const callback = function (mutationsList, observer) {
let hasGameIconChange = false;
for (let mutation of mutationsList) {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'data-src'
) {
const targetElement = mutation.target;
if (targetElement.classList.contains('cdkGameIcon')) {
queueCdkeyGameChecker(targetElement);
hasGameIconChange = true;
}
}
if (mutation.type === 'childList') {
for (let node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.classList && node.classList.contains('cdkGameIcon')) {
queueCdkeyGameChecker(node);
hasGameIconChange = true;
} else if (node.querySelectorAll) {
const icons = node.querySelectorAll('.cdkGameIcon');
if (icons.length > 0) {
icons.forEach((icon) => queueCdkeyGameChecker(icon));
hasGameIconChange = true;
}
}
}
}
if (!hasExecuted && mutation.type === 'childList') {
const balanceTitleElement = document.querySelector(
'.balanceTitle > div'
);
if (balanceTitleElement) {
init();
hasExecuted = true;
}
}
}
// 添加分页按钮点击监听(只添加一次)
if (!paginationListenerAdded) {
const pagination = document.querySelector('.ivu-page');
if (pagination) {
pagination.addEventListener('click', function(e) {
// 检测是否点击了分页按钮(不是由脚本触发的自动翻页)
if (!isAutoSkipping) {
resetAutoSkipState();
}
});
paginationListenerAdded = true;
}
}
// 如果有游戏图标变化,说明页面数据在加载
if (hasGameIconChange) {
if (pageChangeTimer) {
clearTimeout(pageChangeTimer);
}
// 等待游戏数据加载完成后检测
pageChangeTimer = setTimeout(() => {
// 只有在启用了过滤选项时才检测自动跳页
if (noownedGames || noRestrictedGames || noDlc || onlyDelistedGames || noBlacklistGames || noBundledGames || scanHideAllGames) {
checkAndAutoSkip();
}
}, 800); // 减少等待时间,因为不需要等待 SWI 插件渲染
}
};
const observer = new MutationObserver(callback);
observer.observe(document.body, config);
}
//CSS样式
const style = document.createElement('style');
style.innerHTML = `
.ProfileFeaturesLimited {
height: .26rem;
background: #ed4014;
position: absolute;
top: 4px;
left: 34px;
color: #fff;
text-align: center;
line-height: .26rem;
font-size: .11rem;
padding: 0 6px;
border-radius: 3px;
white-space: nowrap;
z-index: 99;
}
#filter > .ivu-checkbox-group > .ivu-checkbox-wrapper {
margin-right: 0 !important;
font-size: 0;
}
#filter > .ivu-checkbox-group > div > .ivu-checkbox-wrapper {
margin-right: 0 !important;
font-size: 0;
}
#filter > .ivu-checkbox-group > .ivu-checkbox-wrapper > .ivu-checkbox,
#filter > .ivu-checkbox-group > div > .ivu-checkbox-wrapper > .ivu-checkbox {
display: none !important;
}
#filter > .ivu-checkbox-group > .ivu-checkbox-wrapper > .ivu-checkbox + span,
#filter > .ivu-checkbox-group > div > .ivu-checkbox-wrapper > .ivu-checkbox + span {
padding: 0 !important;
}
#filter .ivu-tag-default {
border: 1px solid #b8becc !important;
color: #515a6e !important;
background: #f8f8f9 !important;
}
#filter .ivu-tag-default:hover {
border-color: #2d8cf0 !important;
color: #2d8cf0 !important;
}
#filter .ivu-tag-primary {
box-shadow: 0 1px 4px rgba(45,140,240,0.3);
}
#filter .ivu-tag-warning {
box-shadow: 0 1px 4px rgba(255,153,0,0.3);
}
.bsp-settings-modal {
align-items: center;
justify-content: center;
}
.bsp-settings-modal .ivu-modal {
margin: 0 auto;
}
.bsp-settings-modal .ivu-modal-content {
border-radius: 8px;
overflow: visible;
}
.bsp-settings-modal .ivu-modal-body {
padding: 16px;
max-height: calc(100vh - 72px);
overflow: visible;
background: #f5f7fa;
}
.bsp-settings-shell {
color: #344054;
}
.bsp-settings-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
padding: 2px 4px 12px;
border-bottom: 1px solid #e8eaec;
}
.bsp-settings-head h2 {
margin: 0;
color: #17233d;
font-size: 22px;
line-height: 1.2;
}
.bsp-settings-head p {
margin: 4px 0 0;
color: #808695;
font-size: 13px;
}
.bsp-head-actions {
display: flex;
align-items: center;
gap: 8px;
}
.bsp-settings-grid {
display: grid;
grid-template-columns: 330px minmax(0, 1fr);
grid-template-areas:
"owned profile"
"color blacklist"
"page blacklist";
gap: 10px;
}
.bsp-panel {
min-width: 0;
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fff;
}
.bsp-owned-panel { grid-area: owned; }
.bsp-profile-panel { grid-area: profile; }
.bsp-color-panel { grid-area: color; }
.bsp-page-panel { grid-area: page; }
.bsp-blacklist-panel { grid-area: blacklist; }
.bsp-panel-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.bsp-panel-title h3 {
margin: 0;
color: #17233d;
font-size: 16px;
line-height: 1.2;
}
.bsp-panel-title span {
flex: none;
padding: 2px 8px;
border-radius: 999px;
background: #f0f7ff;
color: #2d8cf0;
font-size: 12px;
}
.bsp-muted,
.bsp-last-update {
margin: 0 0 10px;
color: #808695;
font-size: 13px;
line-height: 1.45;
}
.bsp-mini-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-bottom: 8px;
}
.bsp-mini-stats div {
padding: 8px;
border-radius: 6px;
background: #f8f8f9;
}
.bsp-mini-stats span {
display: block;
color: #808695;
font-size: 12px;
}
.bsp-mini-stats strong {
display: block;
margin-top: 2px;
color: #17233d;
font-size: 20px;
line-height: 1.2;
}
.bsp-color-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 10px;
}
.bsp-color-grid label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 30px;
color: #515a6e;
}
.bsp-color-panel {
overflow: visible;
}
.bsp-color-grid .ivu-color-picker-picker {
z-index: 3000 !important;
}
.bsp-blacklist-list {
max-height: 132px;
overflow-y: auto;
margin-bottom: 8px;
border: 1px solid #eef0f3;
border-radius: 6px;
}
.bsp-blacklist-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 6px 8px;
border-bottom: 1px solid #eef0f3;
}
.bsp-blacklist-item:last-child {
border-bottom: 0;
}
.bsp-blacklist-item span {
min-width: 0;
}
.bsp-blacklist-item a {
display: inline-block;
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
white-space: nowrap;
color: #2d8cf0;
}
.bsp-blacklist-item em {
margin-left: 6px;
color: #98a2b3;
font-style: normal;
font-size: 12px;
}
.bsp-empty {
padding: 18px 0;
color: #98a2b3;
text-align: center;
}
.bsp-local-profile-card .ivu-card-body {
padding-top: 16px;
}
.bsp-section-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.bsp-section-title h2 {
margin: 0;
color: #515a6e;
}
.bsp-section-title span {
flex: none;
padding: 2px 8px;
border-radius: 999px;
background: #f0f7ff;
color: #2d8cf0;
font-size: 12px;
font-weight: 500;
}
.bsp-profile-panel {
display: flex;
flex-direction: column;
gap: 10px;
}
.bsp-profile-alert {
margin-bottom: 0 !important;
}
.bsp-profile-controls {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.bsp-switch-line {
min-height: 34px;
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
border-bottom: 1px solid #eef0f3;
color: #515a6e;
}
.bsp-switch-label {
flex: 1;
min-width: 0;
font-weight: 500;
}
.bsp-switch-line .ivu-switch {
flex: none;
}
.bsp-state {
flex: none;
font-style: normal;
font-size: 12px;
color: #808695;
}
.bsp-state-running {
color: #2d8cf0;
}
.bsp-profile-stats {
display: grid;
grid-template-columns: repeat(4, minmax(84px, 1fr));
gap: 7px;
}
.bsp-stat {
min-height: 56px;
padding: 8px;
border: 1px solid #e8eaec;
border-radius: 6px;
background: #f8f8f9;
}
.bsp-stat span {
display: block;
color: #808695;
font-size: 12px;
}
.bsp-stat strong {
display: block;
margin-top: 2px;
color: #17233d;
font-size: 21px;
line-height: 1.2;
}
.bsp-stat-ok {
background: #f3fbf5;
border-color: #c9ebd1;
}
.bsp-stat-bad {
background: #fff5f2;
border-color: #ffd8cf;
}
.bsp-stat-warn {
background: #fff9e6;
border-color: #ffe7a3;
}
.bsp-stat-dlc {
background: #f0faff;
border-color: #bfe7ff;
}
.bsp-stat-nongame {
background: #f6f3ff;
border-color: #d8cef8;
}
.bsp-stat-banned {
background: #fff1f0;
border-color: #ffbdb8;
}
.bsp-stat-pending {
background: #f7f7fb;
border-color: #dcdee6;
}
.bsp-profile-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding-top: 2px;
}
@media (max-width: 720px) {
.bsp-settings-grid {
grid-template-columns: 1fr;
grid-template-areas:
"owned"
"profile"
"color"
"page"
"blacklist";
}
.bsp-profile-controls {
grid-template-columns: 1fr;
}
.bsp-profile-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.bsp-cp-btn {
cursor: pointer;
padding: 4px 10px;
border: 1px solid #dcdee2;
border-radius: 4px;
font-size: 13px;
transition: all 0.15s;
}
.bsp-cp-btn:hover {
border-color: #2d8cf0;
color: #2d8cf0;
}
.bsp-cp-active {
background: #2d8cf0 !important;
color: #fff !important;
border-color: #2d8cf0 !important;
}
`;
document.head.appendChild(style);
//默认颜色
if (!localStorage.getItem('ownedColor')) {
localStorage.setItem('ownedColor', '#0c8918');
localStorage.setItem('wishlistColor', '#177cb0');
localStorage.setItem('familygameColor', '#ff8936');
localStorage.setItem('unownedColor', '#ff2e63');
}
var ownedColor = localStorage.getItem('ownedColor');
var wishlistColor = localStorage.getItem('wishlistColor');
var familygameColor = localStorage.getItem('familygameColor');
var unownedColor = localStorage.getItem('unownedColor');