// ==UserScript== // @name Steam 许可分类器 // @namespace http://tampermonkey.net/ // @version 2.0.2 // @description Classify Steam account licenses: Store, Retail, Gift, Free // @description:zh-CN 对Steam账户许可页面进行分类:商店、零售、礼物、免费 // @description:zh-TW 對Steam帳戶許可頁面進行分類:商店、零售、禮物、贈品 // @author SmallFork // @match https://store.steampowered.com/account/licenses // @match https://store.steampowered.com/account/licenses/* // @grant GM_info // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant unsafeWindow // @connect store.steampowered.com // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/574261/Steam%20%E8%AE%B8%E5%8F%AF%E5%88%86%E7%B1%BB%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/574261/Steam%20%E8%AE%B8%E5%8F%AF%E5%88%86%E7%B1%BB%E5%99%A8.meta.js // ==/UserScript== (function() { 'use strict'; // ==================== 配置常量 ==================== const CONFIG = { PAGE_SIZE: 30,// 每页显示条数 DEBOUNCE_MS: 150,// 搜索输入防抖延迟(毫秒) BLOB_CLEANUP_MS: 500,// Blob URL 清理延迟(毫秒) DATE_COL_WIDTH: 160,// 日期列宽度(像素) MS_PER_DAY: 86400000,// 一天的毫秒数 OBSERVER_DEBOUNCE_MS: 150,// MutationObserver 防抖延迟(毫秒) STICKY_OFFSET: 68,// 表头吸顶偏移量(像素) SEARCH_WIDTH: 450,// 搜索框宽度(像素) SEP_HEIGHT: 30// 分类分隔行高度(像素) }; // ==================== 多语言支持 ==================== const LANG_MAP = { 'zh-cn': '简体', 'zh-tw': '繁体', 'zh-hk': '繁体', 'zh-mo': '繁体', 'en': '英语' }; const ZH = { store: '商店', retail: '零售', free: '免费', gift: '礼物', all: '全部', total: '总计', pageTitle: '{nickname} 的许可', searchPlaceholder: '搜索游戏名称...', showAll: '显示全部', pagedView: '分页显示', prev: '上一页', next: '下一页', historyLink: '消费历史记录', advanced: '高级选项', shortcuts: '快捷键', dateFrom: '日期从', dateTo: '到', sortBy: '排序', nameAsc: '名称 A-Z', nameDesc: '名称 Z-A', dateLabel: '日期', clearFilter: '清除筛选', chartTitle: '许可分类占比', donutTitle: '许可分布详情', exportCSV: 'CSV', exportJSON: 'JSON', exportFailed: '导出失败', redeemBtn: '激活', redeemTitle: 'CDKey 批量激活', redeemPlaceholder: '输入CDKey,支持批量激活', activateBtn: '激活Key', indexLabel: '序号', resultLabel: '结果', detailLabel: '详情', itemLabel: '项目', success: '成功', fail: '失败', redeeming: '激活中', waiting: '等待中', copyFailed: '复制失败Key', noFailedKeys: '没有失败的Key', copiedKeys: '复制成功', networkError: '网络错误或超时', othersError: '其他错误', invalidKey: '无效激活码', duplicateKey: '重复激活', rateLimit: '次数上限', regionRestrict: '地区限制', alreadyOwned: '已拥有', missingDep: '缺少主游戏', walletCode: '这是充值码', withoutFree: '排除免费', yearTitle: '年份分布' }; const I18N = { '简体': ZH, '繁体': { ...ZH, free: '贈品', gift: '禮物', total: '總計', pageTitle: '{nickname} 的授權', searchPlaceholder: '搜尋遊戲名稱...', showAll: '顯示全部', pagedView: '分頁顯示', historyLink: '消費歷史記錄', advanced: '進階選項', dateFrom: '日期從', nameAsc: '名稱 A-Z', nameDesc: '名稱 Z-A', clearFilter: '清除篩選', chartTitle: '許可分類佔比', donutTitle: '許可分佈詳情', exportFailed: '匯出失敗', redeemPlaceholder: '輸入CDKey,支持批量激活', indexLabel: '序號', resultLabel: '結果', detailLabel: '詳情', itemLabel: '項目', fail: '失敗', copyFailed: '複製失敗Key', noFailedKeys: '沒有失敗的Key', copiedKeys: '複製成功', networkError: '網路錯誤或超時', othersError: '其他錯誤', invalidKey: '無效激活碼', duplicateKey: '重複激活', rateLimit: '次數上限', regionRestrict: '地區限制', alreadyOwned: '已擁有', missingDep: '缺少主遊戲', walletCode: '這是充值碼', withoutFree: '排除免費', yearTitle: '年份分佈' }, '英语': { ...ZH, store: 'Store', retail: 'Retail', free: 'Free', gift: 'Gift', all: 'All', total: 'Total', pageTitle: "{nickname}'s Licenses", searchPlaceholder: 'Search game name...', showAll: 'Show All', pagedView: 'Paged', prev: 'Prev', next: 'Next', historyLink: 'Purchase History', advanced: 'Advanced', shortcuts: 'Shortcuts', dateFrom: 'From', dateTo: 'To', sortBy: 'Sort', nameAsc: 'Name A-Z', nameDesc: 'Name Z-A', dateLabel: 'Date', clearFilter: 'Clear Filters', chartTitle: 'License Distribution', donutTitle: 'License Distribution Detail', exportFailed: 'Export Failed', redeemBtn: 'Redeem', redeemTitle: 'Batch CDKey Activation', redeemPlaceholder: 'Enter CDKeys, supports batch activation', activateBtn: 'Redeem Keys', indexLabel: 'No.', resultLabel: 'Result', detailLabel: 'Detail', itemLabel: 'Item', success: 'Success', fail: 'Failed', redeeming: 'Redeeming', waiting: 'Waiting', copyFailed: 'Copy Failed Keys', noFailedKeys: 'No failed keys', copiedKeys: 'Copied successfully', networkError: 'Network error/timeout', othersError: 'Other error', invalidKey: 'Invalid key', duplicateKey: 'Duplicate key', rateLimit: 'Rate limit', regionRestrict: 'Region restricted', alreadyOwned: 'Already owned', missingDep: 'Missing dependency', walletCode: 'Wallet code', withoutFree: 'Without Free', yearTitle: 'Year Distribution' } }; let currentLang = '简体'; function detectLanguage() { const lang = (document.documentElement.lang || '').toLowerCase(); if (LANG_MAP[lang]) return LANG_MAP[lang]; return lang.startsWith('zh') && (lang.includes('tw') || lang.includes('hant')) ? '繁体' : (lang.startsWith('zh') ? '简体' : '英语'); } const t = key => I18N[currentLang]?.[key] ?? ZH[key] ?? key; // ==================== 分类配置 ==================== const MATCH_ORDER = [ { id: 'store', color: '#3b82f6', keywords: ['Steam 商店', 'Steam Store'] }, { id: 'retail', color: '#f59e0b', keywords: ['零售', 'Retail'] }, { id: 'free', color: '#10b981', keywords: ['免费赠送', '贈品', 'Complimentary'] }, { id: 'gift', color: '#ec4899', keywords: ['礼物', '玩家通行证', '禮物', '招待券', 'Gift', 'Guest Pass'] } ]; const CATEGORIES = ['store', 'retail', 'gift', 'free'].map(id => { const cfg = MATCH_ORDER.find(c => c.id === id); return { id, color: cfg.color, get label() { return t(id); } }; }); const ALL_TYPE = Object.freeze({ id: 'all', get label() { return t('all'); }, color: '#4792c4' }); const CATEGORY_MAP = new Map(MATCH_ORDER.map(c => [ new RegExp(c.keywords.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i'), c.id ])); // 图表数据复用:排除免费时的计数与分类 const isWithoutFree = () => state.withoutFree && state.filter !== 'free'; const getChartCounts = () => { if (!isWithoutFree()) return state.counts; const m = new Map(CATEGORIES.map(c => [c.id, 0])); let total = 0; for (const c of CATEGORIES) { if (c.id === 'free') continue; const v = state.counts.get(c.id) || 0; m.set(c.id, v); total += v; } m.set('total', total); return m; }; const getChartCats = () => isWithoutFree() ? CATEGORIES.filter(c => c.id !== 'free') : CATEGORIES; const SELECTORS = { table: 'table.account_table', tableBody: 'table.account_table tbody', pageHeader: 'h2', filterContainer: '#steam-license-filter' }; const VERSION = GM_info.script.version; const disposers = []; let cachedTableBody = null; let cachedChartHash = ''; const state = { filter: 'all', filterClass: '', search: '', page: 1, showAll: false, totalPages: 1, counts: new Map([...CATEGORIES.map(c => [c.id, 0]), ['total', 0]]), dateFrom: '', dateTo: '', sortBy: 'date-desc', withoutFree: false }; // ==================== 样式表 ==================== const STYLES = (() => { const toRgba = (hex, a) => { const v = parseInt(hex.slice(1), 16); return `rgba(${(v >> 16) & 255},${(v >> 8) & 255},${v & 255},${a})`; }; const catRules = CATEGORIES.flatMap(c => [ `table.account_table tbody tr[data-category="${c.id}"]{background:${toRgba(c.color,0.15)}}`, `table.account_table tbody tr[data-category="${c.id}"]:hover{background:${toRgba(c.color,0.25)}}`, `table.account_table tbody.filter-${c.id} tr[data-category]:not([data-category="${c.id}"]){display:none}` ]); const baseBtn = 'border:2px solid transparent;border-radius:6px;background:#2a475e;color:#c7d5e0;cursor:pointer;font-weight:500;transition:all .2s ease;outline:none;will-change:transform'; return [ 'table.account_table{margin-top:0;border-collapse:separate;border-spacing:0;contain:layout paint}', 'table.account_table tr{transition:background-color .2s ease;contain:layout}', `table.account_table th{position:sticky;top:var(--filter-h,${CONFIG.STICKY_OFFSET}px);z-index:99;background:#1b2838;text-transform:none;font-size:13px;font-weight:bold;color:#c6d4df;padding:8px 10px}`, `table.account_table td.license_date_col{width:${CONFIG.DATE_COL_WIDTH}px!important;min-width:${CONFIG.DATE_COL_WIDTH}px;white-space:nowrap;text-align:left}`, `table.account_table th.license_date_col{text-align:left}`, '#steam-license-filter{margin:0;padding:14px 18px;background:linear-gradient(to right,#1b2838,#2a475e);border-radius:8px;display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;box-shadow:0 4px 6px rgba(0,0,0,.3);position:sticky;top:0;z-index:100;contain:layout}', `#steam-license-filter button{${baseBtn}}`, '#steam-license-filter button.filter-btn{padding:8px 0;font-size:14px;width:68px;box-sizing:border-box;height:40px}', '#steam-license-filter button.pager-btn{padding:8px 16px;font-size:13px;height:40px;box-sizing:border-box}', 'button.icon-btn{background:rgba(42,71,94,0.6);border:1px solid rgba(255,255,255,0.08);border-radius:4px;color:#8a9ba8;padding:0 12px;font-size:13px;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:center;height:36px;box-sizing:border-box}', 'button.icon-btn:hover{background:rgba(42,71,94,0.9);color:#c7d5e0;border-color:rgba(255,255,255,0.2)}', 'button.icon-btn svg{width:14px;height:14px}', '#steam-license-filter button.active{background:var(--btn-color,#2a475e);color:#fff;border-color:var(--btn-color,#2a475e)}', `#slc-advanced-panel button.panel-btn{${baseBtn};padding:8px 14px;font-size:13px}`, '#slc-advanced-panel button.panel-btn svg{vertical-align:middle}', '#slc-advanced-panel button.panel-btn:hover{background:rgba(71,146,196,.8);color:#fff;border-color:rgba(71,146,196,.8);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}', '#steam-license-filter button:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}', ...catRules, 'table.account_table tbody.paginated tr[data-category]:not(.page-visible){display:none}', 'table.account_table tbody tr[data-category="other"]{display:none}', 'h2 .search-box{background:rgba(0,0,0,.2);border:1px solid #4a6a7a;border-radius:6px;padding:8px 14px;color:#c7d5e0;font-size:14px;outline:none;transition:all .2s ease}', 'h2 .search-box::placeholder{color:#8a9ba8}', 'h2 .search-box:focus,#slc-advanced-panel input[type="date"]:focus,#slc-advanced-panel select:focus{border-color:#66c0f4;background:rgba(0,0,0,.3);box-shadow:0 0 0 2px rgba(102,192,244,.2)}', 'table.account_table tbody.search-filtered tr[data-category]:not(.search-match){display:none}', '#steam-license-filter button[data-action]:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,.4);background:#3d6f8e}', '#steam-license-filter button[data-action]:disabled{opacity:.4;cursor:not-allowed}', '#steam-license-filter button[data-action].active-mode{background:#4792c4;color:#fff}', '#license-stats{color:#c7d5e0;font-size:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;contain:layout}', '.page-info{display:inline-flex;align-items:center;gap:2px;font-size:13px;font-weight:500;white-space:nowrap}', '#slc-advanced-panel{padding:14px 18px;background:linear-gradient(to right,#1b2838,#2a475e);border-radius:8px;margin-bottom:10px;font-size:13px;color:#c7d5e0;box-shadow:0 4px 6px rgba(0,0,0,.3);contain:layout}', '#slc-advanced-panel label{display:flex;align-items:center;gap:6px;cursor:pointer}', '#slc-advanced-panel input[type="date"],#slc-advanced-panel select{background:rgba(0,0,0,.2);border:1px solid #4a6a7a;border-radius:6px;color:#c7d5e0;padding:8px 14px;font-size:13px;outline:none;transition:all .2s ease;color-scheme:dark}', '#slc-advanced-panel select option{background:#1b2838;color:#c7d5e0}', '#slc-without-free-label{position:relative;padding-left:36px;min-height:20px;line-height:20px}', '#slc-without-free-label input[type="checkbox"]{position:absolute;opacity:0;width:0;height:0}', '#slc-without-free-label .toggle-track{position:absolute;left:0;top:50%;transform:translateY(-50%);width:32px;height:18px;background:#2a475e;border:1px solid #4a6a7a;border-radius:9px;cursor:pointer;transition:all .2s ease}', '#slc-without-free-label .toggle-track::after{content:"";position:absolute;left:2px;top:2px;width:12px;height:12px;background:#8a9ba8;border-radius:50%;transition:all .2s ease}', '#slc-without-free-label input:checked+.toggle-track{background:rgba(102,192,244,.3);border-color:#66c0f4}', '#slc-without-free-label input:checked+.toggle-track::after{left:16px;background:#66c0f4}', '#steam-license-filter kbd{background:#2a475e;border:1px solid #4a6a7a;border-radius:3px;padding:2px 6px;font-size:11px}', '#slc-chart-bar{display:flex;align-items:center;gap:0;height:20px;border-radius:3px;overflow:hidden;contain:layout}', '#slc-chart-bar .bar-seg{height:100%;transition:width .3s ease}', '#slc-donut-modal,#slc-redeem-modal{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:10000;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:all .3s ease}', '#slc-donut-modal.visible,#slc-redeem-modal.visible{opacity:1;visibility:visible}', '#slc-donut-content,#slc-redeem-content{background:linear-gradient(135deg,#1b2838,#2a475e);border-radius:16px;padding:30px;box-shadow:0 8px 32px rgba(0,0,0,.5);width:90%;position:relative}', '#slc-donut-content{max-width:500px;contain:layout}', '#slc-redeem-content{max-width:750px;max-height:85vh;overflow-y:auto;contain:content}', '#slc-donut-close,#slc-redeem-close{position:absolute;top:12px;right:12px;background:none;border:none;color:#8a9ba8;font-size:24px;cursor:pointer;padding:4px 8px;border-radius:4px;transition:all .2s}', '#slc-donut-close:hover,#slc-redeem-close:hover{background:rgba(255,255,255,.1);color:#c7d5e0}', '#slc-donut-chart{display:flex;align-items:center;justify-content:center;margin:20px 0}', '#slc-donut-legend{display:flex;flex-direction:column;gap:8px;margin-top:16px}', '#slc-donut-legend .legend-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:rgba(0,0,0,.2);border-radius:8px;transition:all .2s}', '#slc-donut-legend .legend-item:hover{background:rgba(0,0,0,.3);transform:translateX(4px)}', '#slc-donut-legend .legend-color{width:16px;height:16px;border-radius:4px;flex-shrink:0}', '#slc-donut-legend .legend-label{flex:1;color:#c7d5e0;font-size:14px}', '#slc-donut-legend .legend-value{color:#8a9ba8;font-size:13px}', '#slc-year-modal{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:10000;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:all .3s ease}', '#slc-year-modal.visible{opacity:1;visibility:visible}', '#slc-year-content{background:linear-gradient(135deg,#1b2838,#2a475e);border-radius:16px;padding:30px;box-shadow:0 8px 32px rgba(0,0,0,.5);width:90%;max-width:800px;position:relative;contain:layout}', '#slc-year-close{position:absolute;top:12px;right:12px;background:none;border:none;color:#8a9ba8;font-size:24px;cursor:pointer;padding:4px 8px;border-radius:4px;transition:all .2s}', '#slc-year-close:hover{background:rgba(255,255,255,.1);color:#c7d5e0}', '#slc-year-chart{margin:16px 0}', '#slc-year-legend{display:flex;gap:16px;justify-content:center;margin-top:12px;flex-wrap:wrap}', '#slc-year-legend .yl-item{display:flex;align-items:center;gap:6px;font-size:13px;color:#c7d5e0}', '#slc-year-legend .yl-color{width:12px;height:12px;border-radius:2px;flex-shrink:0}', '#slc-redeem-content .modal-title{font-size:18px;font-weight:bold;color:#c7d5e0;margin-bottom:16px;display:flex;align-items:center;gap:8px}', '#slc-redeem-content .modal-title svg{width:22px;height:22px;flex-shrink:0}', '#slc-redeem-content .redeem-actions{display:flex;gap:8px;margin-top:16px;justify-content:flex-end}', '#slc-redeem-table{width:100%;border-collapse:collapse;font-size:13px;margin-top:12px}', '#slc-redeem-table th{padding:8px 10px;text-align:left;color:#8f98a0;font-size:12px;text-transform:uppercase;letter-spacing:.5px;background:rgba(0,0,0,.4);border-bottom:1px solid rgba(255,255,255,.08)}', '#slc-redeem-table td{padding:7px 10px;border-bottom:1px solid rgba(255,255,255,.05);color:#c7d5e0;vertical-align:middle}', '#slc-redeem-table tr:hover td{background:rgba(255,255,255,.03)}', '#slc-redeem-table .redeem-success td{background:rgba(16,185,129,.4)}', '#slc-redeem-table .redeem-success:hover td{background:rgba(16,185,129,.5)}', '#slc-redeem-table .redeem-fail td{background:rgba(239,68,68,.4)}', '#slc-redeem-table .redeem-fail:hover td{background:rgba(239,68,68,.5)}', '#slc-redeem-table code{color:#66c0f4;font-size:13px;background:rgba(0,0,0,.2);padding:1px 5px;border-radius:3px}', '#slc-redeem-table a{color:#66c0f4;text-decoration:none;transition:color .15s}', '#slc-redeem-table a:hover{color:#fff}', '#slc-redeem-content textarea,#slc-redeem-content input[type="text"]{background:rgba(0,0,0,.3);border:1px solid #4a6a7a;border-radius:6px;padding:8px 14px;color:#c7d5e0;font-size:13px;outline:none;transition:all .2s ease;box-sizing:border-box}', '#slc-redeem-content textarea:focus,#slc-redeem-content input[type="text"]:focus{border-color:#66c0f4;background:rgba(0,0,0,.4);box-shadow:0 0 0 2px rgba(102,192,244,.2)}', '#slc-redeem-content button.redeem-btn{border:none;border-radius:4px;padding:8px 20px;font-size:13px;font-weight:500;cursor:pointer;transition:all .2s ease;outline:none;will-change:transform}', '#slc-redeem-content button.redeem-btn:hover{transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,.4)}', '#slc-redeem-content button.redeem-btn:active{transform:translateY(0)}', '#slc-redeem-content button.redeem-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none}', '#slc-redeem-content button.redeem-btn-primary{background:linear-gradient(135deg,#4792c4,#3a78b0);color:#fff}', '#slc-redeem-content button.redeem-btn-primary:hover:not(:disabled){background:linear-gradient(135deg,#5aa8d8,#4a8ec0)}', '#slc-redeem-content button.redeem-btn-secondary{background:rgba(42,71,94,.8);color:#c7d5e0;border:1px solid rgba(255,255,255,.1)}', '#slc-redeem-content button.redeem-btn-secondary:hover:not(:disabled){background:rgba(42,71,94,1);border-color:rgba(255,255,255,.2)}' ].join(''); })(); // ==================== 工具函数 ==================== function getTableBody() { if (cachedTableBody && !cachedTableBody.isConnected) { cachedTableBody = null; startObserver(); } return cachedTableBody || (cachedTableBody = document.querySelector(SELECTORS.tableBody)); } function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const existing = document.querySelector(selector); if (existing) return resolve(existing); let settled = false; const settle = el => { if (settled) return; settled = true; obs.disconnect(); clearTimeout(timer); resolve(el); }; const observeRoot = document.querySelector('#page_body') || document.documentElement; const obs = new MutationObserver(() => { const el = document.querySelector(selector); if (el) settle(el); }); obs.observe(observeRoot, { childList: true, subtree: true }); const timer = setTimeout(() => { if (!settled) { settled = true; obs.disconnect(); reject(new Error(`waitForElement timeout: ${selector}`)); } }, timeout); }); } function classifyByAcquisitionMethod(text) { const s = text.trim(); for (const [re, id] of CATEGORY_MAP) { if (re.test(s)) return id; } return 'other'; } function isHeaderRow(row) { if (row.querySelector('th') !== null) return true; const dateText = row.querySelector('td.license_date_col')?.textContent?.trim() || ''; return dateText.length > 0 && !/\d/.test(dateText); } function getDataRows() { const tb = getTableBody(); return tb ? Array.from(tb.rows).filter(row => !isHeaderRow(row)) : []; } const MONTH_ABBR = { jan:0,feb:1,mar:2,apr:3,may:4,jun:5,jul:6,aug:7,sep:8,oct:9,nov:10,dec:11 }; function parseDate(str) { const s = str.trim(); // 中文:2024年1月15日 const mZh = s.match(/(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日/); if (mZh) return new Date(+mZh[1], +mZh[2] - 1, +mZh[3]).getTime(); // 英文格式1(月在前):Jan 15, 2024 — 必须在格式2之前匹配,否则 "2 May" 的 "2" 会被误识别为月份 const mEn = s.match(/([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})/); if (mEn) { const mon = MONTH_ABBR[mEn[1].toLowerCase().slice(0, 3)]; if (mon !== undefined) return new Date(+mEn[3], mon, +mEn[2]).getTime(); } // 英文格式2(日在前):2 May, 2026 const mEn2 = s.match(/(\d{1,2})\s+([A-Za-z]+),?\s+(\d{4})/); if (mEn2) { const mon = MONTH_ABBR[mEn2[2].toLowerCase().slice(0, 3)]; if (mon !== undefined) return new Date(+mEn2[3], mon, +mEn2[1]).getTime(); } const d = new Date(s); return isNaN(d) ? 0 : d.getTime(); } // ==================== DOM工厂函数 ==================== const BOOL_ATTRS = new Set([ 'disabled', 'checked', 'selected', 'readonly', 'required', 'hidden', 'multiple', 'autofocus', 'autoplay', 'controls', 'loop', 'muted', 'open', 'reversed', 'allowfullscreen', 'default' ]); function createEl(tag, props = {}, children = []) { const el = document.createElement(tag); for (const [k, v] of Object.entries(props)) { if (v === null || v === undefined) continue; if (k === 'style' && typeof v === 'object') { for (const [sk, sv] of Object.entries(v)) { if (sk.startsWith('--')) el.style.setProperty(sk, sv); else el.style[sk] = sv; } } else if (k === 'dataset') Object.assign(el.dataset, v); else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v); else if (k === 'text') el.textContent = v; else if (k === 'html') el.innerHTML = v; else if (BOOL_ATTRS.has(k)) { if (v) el.setAttribute(k, ''); else el.removeAttribute(k); } else el.setAttribute(k, v); } for (const child of children) { if (typeof child === 'string') el.appendChild(document.createTextNode(child)); else el.appendChild(child); } return el; } // ==================== 昵称功能 ==================== function getNickname() { return document.querySelector('#account_pulldown')?.textContent.trim() || document.querySelector('#global_header .user_persona')?.textContent.trim() || ''; } function updatePageTitle() { const nickname = getNickname(); if (!nickname) return; const pageHeader = document.querySelector(SELECTORS.pageHeader); if (!pageHeader) return; const originalTitle = pageHeader.dataset.originalTitle || pageHeader.textContent.trim(); const newTitle = t('pageTitle').replace('{nickname}', nickname); let titleSpan = pageHeader.querySelector('.page-title-text'); if (!titleSpan) { titleSpan = createEl('span', { class: 'page-title-text', text: newTitle }); pageHeader.textContent = ''; pageHeader.appendChild(titleSpan); pageHeader.style.cssText = 'display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer'; titleSpan.style.cssText = 'max-width:500px;overflow-wrap:break-word'; } if (!pageHeader.dataset.titleInitialized) { pageHeader.dataset.titleInitialized = 'true'; const clickHandler = e => { if (e.target.closest('.search-box')) return; const showOriginal = pageHeader.dataset.showOriginal === 'true'; titleSpan.textContent = showOriginal ? pageHeader.dataset.newTitle : pageHeader.dataset.originalTitle; pageHeader.dataset.showOriginal = showOriginal ? 'false' : 'true'; }; pageHeader.addEventListener('click', clickHandler); disposers.push(() => pageHeader.removeEventListener('click', clickHandler)); } else { titleSpan.textContent = newTitle; } pageHeader.dataset.originalTitle = originalTitle; pageHeader.dataset.newTitle = newTitle; document.title = newTitle; } function addBreadcrumbLink() { const currentSpan = document.querySelector('.blockbg .breadcrumb_current_page'); if (!currentSpan || currentSpan.dataset.breadcrumbAdded) return; currentSpan.dataset.breadcrumbAdded = 'true'; const parent = currentSpan.parentElement; parent.appendChild(createEl('span', { class: 'breadcrumb_separator', text: '>' })); parent.appendChild(createEl('a', { href: 'https://store.steampowered.com/account/history', text: t('historyLink'), 'data-panel': '{"noFocusRing":true}' })); } // ==================== UI引用 ==================== const ui = { btns: new Map(), statTotal: null, statCounts: new Map(), pageTotal: null, prevBtn: null, nextBtn: null, showAllBtn: null, pageInput: null, searchInput: null, advancedPanel: null, chartContainer: null, donutModal: null, yearModal: null, resizeObs: null }; // ==================== 核心逻辑 ==================== function markRows() { const tableBody = getTableBody(); if (!tableBody) return; for (const row of tableBody.rows) { if (isHeaderRow(row)) { delete row.dataset.category; delete row.dataset.dateTimestamp; delete row.dataset.searchText; continue; } if (row.dataset.category) continue; const dateCell = row.querySelector('td.license_date_col'); const nameCell = row.querySelector('td:not(.license_date_col):not(.license_acquisition_col):not(.steamdb_license_id_col)'); const acqCell = row.querySelector('td.license_acquisition_col'); if (!dateCell || !nameCell || !acqCell) continue; const rawDate = dateCell.textContent.trim(); row.dataset.dateTimestamp = parseDate(rawDate); const normalizedDate = rawDate.replace(/\s+/g, ' ').trim().replace(/ *([年月日]) */g, '$1'); if (dateCell.textContent !== normalizedDate) dateCell.textContent = normalizedDate; row.dataset.category = classifyByAcquisitionMethod(acqCell.textContent); // 缓存游戏名称和搜索文本,避免排序和搜索时重复 DOM 查询 row.dataset.gameName = nameCell?.textContent?.trim() || ''; row.dataset.searchText = row.dataset.gameName.toLowerCase(); } } function recalcCounts() { for (const k of state.counts.keys()) state.counts.set(k, 0); for (const row of getDataRows()) { const cat = row.dataset.category; if (cat && state.counts.has(cat)) { state.counts.set(cat, state.counts.get(cat) + 1); state.counts.set('total', state.counts.get('total') + 1); } } cachedChartHash = ''; } function refreshNumbers(visibleRows, catCounts) { const isSearching = !!state.search || state.dateFrom || state.dateTo || state.withoutFree; ui.statTotal.textContent = isSearching ? visibleRows.length : state.counts.get('total'); for (const c of CATEGORIES) { const el = ui.statCounts.get(c.id); if (el) el.textContent = isSearching ? (catCounts.get(c.id) || 0) : state.counts.get(c.id); } } function lockStatWidths() { const digits = String(state.counts.get('total')).length; if (ui.statTotal) ui.statTotal.style.minWidth = `${digits}ch`; for (const [, el] of ui.statCounts) el.style.minWidth = `${Math.max(1, digits - 1)}ch`; } function applyFilter(filterType) { const tb = getTableBody(); if (!tb) return; state.filter = filterType; if (state.filterClass) tb.classList.remove(state.filterClass); state.filterClass = filterType !== 'all' ? `filter-${filterType}` : ''; if (state.filterClass) tb.classList.add(state.filterClass); ui.btns.forEach((b, id) => b.classList.toggle('active', id === filterType)); state.page = 1; applyPagination(); } function sortRows(rows) { const [field, dir] = state.sortBy.split('-'); return rows.sort((a, b) => { let cmp = 0; if (field === 'date') { cmp = (+a.dataset.dateTimestamp || 0) - (+b.dataset.dateTimestamp || 0); } else if (field === 'name') { cmp = (a.dataset.gameName || '').localeCompare(b.dataset.gameName || ''); } return dir === 'asc' ? cmp : -cmp; }); } function getVisibleRows() { const allDataRows = getDataRows(); const fromTime = state.dateFrom ? new Date(state.dateFrom).getTime() : 0; const toTime = state.dateTo ? new Date(state.dateTo).getTime() + CONFIG.MS_PER_DAY : Infinity; const searchLower = state.search; const filterCat = state.filter; const visibleRows = []; const catCounts = new Map(CATEGORIES.map(c => [c.id, 0])); for (const row of allDataRows) { const cat = row.dataset.category; if (!cat || cat === 'other') continue; const rowDate = +row.dataset.dateTimestamp || 0; if (rowDate && (rowDate < fromTime || rowDate > toTime)) { row.classList.remove('search-match'); continue; } // 使用缓存的 searchText,避免重复 toLowerCase() const matchSearch = !searchLower || (row.dataset.searchText || '').includes(searchLower); row.classList.toggle('search-match', matchSearch); if (!matchSearch) continue; if (filterCat !== 'all' ? cat !== filterCat : state.withoutFree && cat === 'free') continue; visibleRows.push(row); catCounts.set(cat, (catCounts.get(cat) || 0) + 1); } if (state.sortBy !== 'date-desc') sortRows(visibleRows); return { visibleRows, catCounts }; } function applyPagination() { const tb = getTableBody(); if (!tb) return; const { visibleRows, catCounts } = getVisibleRows(); const hasFilter = !!state.search || state.dateFrom || state.dateTo; tb.classList.toggle('search-filtered', hasFilter); refreshNumbers(visibleRows, catCounts); updateChart(); if (state.showAll) { tb.classList.remove('paginated'); state.totalPages = 1; for (const row of tb.rows) row.classList.remove('page-visible'); } else { tb.classList.add('paginated'); state.totalPages = Math.max(1, Math.ceil(visibleRows.length / CONFIG.PAGE_SIZE)); state.page = Math.min(state.totalPages, Math.max(1, state.page)); const start = (state.page - 1) * CONFIG.PAGE_SIZE, end = start + CONFIG.PAGE_SIZE; const visibleSet = new Set(visibleRows.slice(start, end)); for (const row of tb.rows) { if (!row.dataset.category) continue; row.classList.toggle('page-visible', visibleSet.has(row)); } } updatePagerUI(); } // ==================== 分页控件 ==================== function createPagerParts() { const prevBtn = createEl('button', { class: 'pager-btn', 'data-action': 'prev', text: t('prev') }); const pageInput = createEl('input', { type: 'text', class: 'page-input', style: { width: '30px', textAlign: 'center', background: 'rgba(0,0,0,.3)', border: '1px solid #4a6a7a', borderRadius: '4px', color: '#c7d5e0', fontSize: '13px', padding: '2px 4px', outline: 'none' } }); const totalSpan = createEl('span', { style: { color: '#c7d5e0', minWidth: '30px', display: 'inline-block', textAlign: 'center' } }); const sepSpan = createEl('span', { style: { color: '#8a9ba8' }, text: ' / ' }); const infoWrap = createEl('span', { class: 'page-info' }, [pageInput, sepSpan, totalSpan]); const nextBtn = createEl('button', { class: 'pager-btn', 'data-action': 'next', text: t('next') }); const showAllBtn = createEl('button', { class: 'pager-btn', 'data-action': 'showAll', text: t('showAll') }); pageInput.addEventListener('focus', () => pageInput.style.borderColor = '#66c0f4'); pageInput.addEventListener('blur', () => { pageInput.style.borderColor = '#4a6a7a'; handlePageInput(pageInput); }); pageInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); pageInput.blur(); } }); ui.prevBtn = prevBtn; ui.nextBtn = nextBtn; ui.showAllBtn = showAllBtn; ui.pageTotal = totalSpan; ui.pageInput = pageInput; return { prevBtn, infoWrap, nextBtn, showAllBtn }; } function handlePageInput(input) { const val = parseInt(input.value, 10); if (!isNaN(val) && val >= 1 && val <= state.totalPages && val !== state.page) { state.page = val; applyPagination(); } else { input.value = state.page; } } function updatePagerUI() { if (!ui.prevBtn) return; if (state.showAll) { ui.showAllBtn.textContent = t('pagedView'); ui.showAllBtn.classList.add('active-mode'); ui.prevBtn.disabled = ui.nextBtn.disabled = true; ui.pageInput.value = 1; ui.pageInput.disabled = true; } else { ui.showAllBtn.textContent = t('showAll'); ui.showAllBtn.classList.remove('active-mode'); ui.prevBtn.disabled = state.page <= 1; ui.nextBtn.disabled = state.page >= state.totalPages; ui.pageInput.value = state.page; ui.pageInput.disabled = false; } ui.pageTotal.textContent = state.totalPages; } // ==================== 图表 ==================== function updateChart() { if (!ui.chartContainer) return; if (!ui.advancedPanel || ui.advancedPanel.style.display === 'none') { ui.chartContainer.style.display = 'none'; return; } const counts = getChartCounts(); const hash = CATEGORIES.map(c => counts.get(c.id)).join(','); if (hash === cachedChartHash) { ui.chartContainer.style.display = 'block'; return; } cachedChartHash = hash; ui.chartContainer.style.display = 'block'; const total = counts.get('total') || 1; const cats = getChartCats(); const parts = cats.map(c => { const count = counts.get(c.id) || 0; const pct = (count / total * 100).toFixed(1); const label = escHtml(c.label); return { bar: `
`, legend: `${label} ${pct}%` }; }); ui.chartContainer.innerHTML = `
${t('chartTitle')}:${parts.map(p => p.legend).join('')}${t('shortcuts')}: / ${t('searchPlaceholder')} 1-5 ${t('all')}/${t('store')}/${t('retail')}/${t('gift')}/${t('free')} ←→ ${t('prev')}/${t('next')} A ${t('showAll')}/${t('pagedView')} ~ ${t('advanced')}
${parts.map(p => p.bar).join('')}
`; } // ==================== 环形图 ==================== function showDonutChart() { if (!ui.donutModal) { ui.donutModal = document.createElement('div'); ui.donutModal.id = 'slc-donut-modal'; ui.donutModal.innerHTML = `
`; document.body.appendChild(ui.donutModal); ui.donutModal.addEventListener('click', e => { if (e.target === ui.donutModal) hideDonutChart(); }); ui.donutModal.querySelector('#slc-donut-close').addEventListener('click', hideDonutChart); disposers.push(() => { ui.donutModal?.remove(); ui.donutModal = null; }); } const counts = getChartCounts(); const total = counts.get('total') || 0; const hash = CATEGORIES.map(c => counts.get(c.id)).join(','); if (hash === ui.donutModal.dataset.hash) { ui.donutModal.classList.add('visible'); return; } ui.donutModal.dataset.hash = hash; const cats = getChartCats(); const data = cats.map(c => ({ ...c, count: counts.get(c.id) || 0 })).filter(d => d.count > 0); ui.donutModal.querySelector('#slc-donut-title').textContent = t('donutTitle'); ui.donutModal.querySelector('#slc-donut-total').textContent = `${t('total')}: ${total}`; const svgSize = 200, sw = 40, r = (svgSize - sw) / 2, C = 2 * Math.PI * r; let offset = 0; const segments = data.map(d => { const dash = (d.count / total) * C; const seg = ``; offset += dash; return seg; }).join(''); ui.donutModal.querySelector('#slc-donut-chart').innerHTML = `${segments}${total}${escHtml(t('total'))}`; ui.donutModal.querySelector('#slc-donut-legend').innerHTML = data.map(d => `
${escHtml(d.label)}
${d.count} (${(d.count / total * 100).toFixed(1)}%)
`).join(''); ui.donutModal.classList.add('visible'); } function hideDonutChart() { ui.donutModal?.classList.remove('visible'); } // ==================== 年份分布柱状图 ==================== function showYearChart() { if (!ui.yearModal) { ui.yearModal = document.createElement('div'); ui.yearModal.id = 'slc-year-modal'; ui.yearModal.innerHTML = `
`; document.body.appendChild(ui.yearModal); ui.yearModal.addEventListener('click', e => { if (e.target === ui.yearModal) hideYearChart(); }); ui.yearModal.querySelector('#slc-year-close').addEventListener('click', hideYearChart); disposers.push(() => { ui.yearModal?.remove(); ui.yearModal = null; }); } // 收集年份-分类数据 const cats = isWithoutFree() ? CATEGORIES.filter(c => c.id !== 'free') : CATEGORIES; const yearMap = new Map(); for (const row of getDataRows()) { const cat = row.dataset.category; if (!cat || cat === 'other') continue; if (isWithoutFree() && cat === 'free') continue; const ts = +row.dataset.dateTimestamp || 0; if (!ts) continue; const year = new Date(ts).getFullYear(); if (!yearMap.has(year)) yearMap.set(year, new Map(cats.map(c => [c.id, 0]))); const yData = yearMap.get(year); if (yData.has(cat)) yData.set(cat, yData.get(cat) + 1); } if (!yearMap.size) { ui.yearModal.classList.add('visible'); ui.yearModal.querySelector('#slc-year-chart').innerHTML = '
No data
'; return; } const years = [...yearMap.keys()].sort((a, b) => a - b); const maxY = Math.max(...years.map(y => [...yearMap.get(y).values()].reduce((s, v) => s + v, 0))); // SVG 参数 const svgW = 700, svgH = 380; const padL = 50, padR = 20, padT = 20, padB = 40; const chartW = svgW - padL - padR, chartH = svgH - padT - padB; const barW = Math.min(40, (chartW / years.length) * 0.6); const gap = chartW / years.length; // Y轴刻度 const yTicks = 5; let gridLines = ''; for (let i = 0; i <= yTicks; i++) { const val = Math.round(maxY / yTicks * i); const y = padT + chartH - (chartH * i / yTicks); gridLines += ``; gridLines += `${val}`; } // 绘制柱状图 let bars = ''; years.forEach((year, i) => { const yData = yearMap.get(year); const cx = padL + gap * i + gap / 2; let stackedH = 0; const total = [...yData.values()].reduce((s, v) => s + v, 0); // 从底部向上:store, retail, gift, free for (const c of cats) { const count = yData.get(c.id) || 0; const h = maxY ? (count / maxY) * chartH : 0; const y = padT + chartH - stackedH - h; bars += `${year} ${escHtml(c.label)}: ${count} (${(count / total * 100).toFixed(1)}%)`; stackedH += h; } // X轴年份 bars += `${year}`; }); ui.yearModal.querySelector('#slc-year-title').textContent = t('yearTitle'); ui.yearModal.querySelector('#slc-year-chart').innerHTML = `${gridLines}${bars}`; ui.yearModal.querySelector('#slc-year-legend').innerHTML = cats.map(c => `${escHtml(c.label)}`).join(''); ui.yearModal.classList.add('visible'); } function hideYearChart() { ui.yearModal?.classList.remove('visible'); } // ==================== CDKey 激活 ==================== const REDEEM_CFG = { MAX_CONCURRENT: 9, WAIT_SECONDS: 20 }; const FAILURE_DETAILS = { 14:'invalidKey',15:'duplicateKey',53:'rateLimit',13:'regionRestrict',9:'alreadyOwned',24:'missingDep',50:'walletCode' }; const redeemState = { keyCount: 0, recvCount: 0, failedKeys: [], failedKeySet: new Set(), modal: null, batchTimer: null, rowMap: new Map() }; function getSessionID() { try { return unsafeWindow.g_sessionID || window.g_sessionID || ''; } catch { return window.g_sessionID || ''; } } function extractKeys(text) { const matches = (text || '').trim().toUpperCase().match(/([0-9A-Z]{5}-){2,4}[0-9A-Z]{5}/g); return [...new Set(matches || [])]; } // HTML 转义,防止 XSS(CDKey 和游戏名等用户数据插入 innerHTML 时使用) const _escDiv = document.createElement('div'); function escHtml(s) { _escDiv.textContent = s; return _escDiv.innerHTML; } function showRedeemModal() { if (!redeemState.modal) createRedeemModal(); redeemState.modal.classList.add('visible'); } function hideRedeemModal() { redeemState.modal?.classList.remove('visible'); } function setRedeemDone() { if (redeemState.recvCount >= redeemState.keyCount) { const m = redeemState.modal; const btn = m.querySelector('#slc-redeem-activate'); const inp = m.querySelector('#slc-redeem-input'); if (btn) btn.disabled = false; if (inp) inp.disabled = false; } } function createRedeemModal() { const KEY_SVG = ''; const modal = document.createElement('div'); modal.id = 'slc-redeem-modal'; modal.innerHTML = `
`; document.body.appendChild(modal); modal.addEventListener('click', e => { if (e.target === modal) e.stopPropagation(); }); modal.querySelector('#slc-redeem-close').addEventListener('click', hideRedeemModal); modal.querySelector('#slc-redeem-copy-failed').addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); if (redeemState.failedKeys.length) { GM_setClipboard(redeemState.failedKeys.join(',')); alert(`${t('copiedKeys')} (${redeemState.failedKeys.length})`); } else { alert(t('noFailedKeys')); } }); modal.querySelector('#slc-redeem-activate').addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); const keys = extractKeys(modal.querySelector('#slc-redeem-input').value); if (keys.length) doRedeemKeys(keys); }); redeemState.modal = modal; disposers.push(() => { clearInterval(redeemState.batchTimer); modal?.remove(); redeemState.modal = null; }); } function doRedeemKeys(keys) { const m = redeemState.modal; const tbody = m.querySelector('#slc-redeem-tbody'); m.querySelector('#slc-redeem-table').style.display = ''; m.querySelector('#slc-redeem-activate').disabled = true; m.querySelector('#slc-redeem-input').disabled = true; redeemState.keyCount = keys.length; redeemState.recvCount = 0; redeemState.failedKeys = []; redeemState.failedKeySet.clear(); redeemState.rowMap.clear(); keys.forEach((key, i) => { const row = document.createElement('tr'); row.dataset.key = key; row.innerHTML = `${i + 1} ${escHtml(key)} ${escHtml(t('redeeming'))}... `; tbody.prepend(row); redeemState.rowMap.set(key, row); if (i < REDEEM_CFG.MAX_CONCURRENT) { redeemSingleKey(key); } else { const cell = row.querySelector('[data-status]'); cell.textContent = `${t('waiting')} (${REDEEM_CFG.WAIT_SECONDS}s)...`; cell.dataset.waiting = 'true'; } }); if (keys.length > REDEEM_CFG.MAX_CONCURRENT) startBatchTimer(); } function redeemSingleKey(key) { const fail = (detail, reasonKey = 'networkError') => updateRedeemRow(key, t('fail'), detail, reasonKey, 0, ''); GM_xmlhttpRequest({ url: 'https://store.steampowered.com/account/ajaxregisterkey/', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Origin: 'https://store.steampowered.com', Referer: 'https://store.steampowered.com/account/registerkey' }, data: `product_key=${key}&sessionid=${getSessionID()}`, method: 'POST', responseType: 'json', onload(resp) { if (resp.status !== 200 || !resp.response) { fail(t('networkError')); return; } const d = resp.response, it = d.purchase_receipt_info?.line_items?.[0]; if (d.success === 1) { updateRedeemRow(key, t('success'), '\u2014\u2014', '', Number(it?.packageid) || 0, it?.line_item_description || ''); } else { const reasonKey = FAILURE_DETAILS[d.purchase_result_details] || 'othersError'; updateRedeemRow(key, t('fail'), t(reasonKey), reasonKey, Number(it?.packageid) || 0, it?.line_item_description || ''); } }, ontimeout: () => fail(t('networkError')), onerror: () => fail(t('networkError')) }); } function updateRedeemRow(key, result, detail, reasonKey, subId, subName) { const isSuccess = result === t('success'); const isFail = result === t('fail'); handleFailedKey(key, isSuccess); redeemState.recvCount++; const row = redeemState.rowMap.get(key); if (!row) return; row.className = isSuccess ? 'redeem-success' : (isFail ? 'redeem-fail' : ''); const cells = row.querySelectorAll('td'); cells[2].textContent = result; cells[2].removeAttribute('data-status'); cells[2].removeAttribute('style'); cells[3].textContent = detail; cells[4].innerHTML = subId ? `${escHtml(String(subId))}` : '\u2014\u2014'; cells[5].textContent = subName || '\u2014\u2014'; setRedeemDone(); } function handleFailedKey(key, success) { if (success) { if (redeemState.failedKeySet.delete(key)) redeemState.failedKeys = redeemState.failedKeys.filter(k => k !== key); } else if (!redeemState.failedKeySet.has(key)) { redeemState.failedKeySet.add(key); redeemState.failedKeys.push(key); } } function startBatchTimer() { clearInterval(redeemState.batchTimer); redeemState.batchTimer = setInterval(() => { const waiting = redeemState.modal.querySelectorAll('#slc-redeem-tbody tr [data-waiting="true"]'); if (!waiting.length) { clearInterval(redeemState.batchTimer); redeemState.batchTimer = null; return; } let activated = 0; for (const cell of waiting) { if (activated >= REDEEM_CFG.MAX_CONCURRENT) return; cell.textContent = `${t('redeeming')}...`; cell.removeAttribute('data-waiting'); redeemSingleKey(cell.closest('tr').dataset.key); activated++; } }, REDEEM_CFG.WAIT_SECONDS * 1000); } // ==================== 数据导出 ==================== function getExportData() { const actionRe = /\s*(移除|Remove|删除|Delete|卸载|Uninstall)\s*/gi; return getDataRows().filter(r => r.dataset.category && r.dataset.category !== 'other' && !(state.withoutFree && r.dataset.category === 'free')).map(r => ({ date: r.querySelector('td.license_date_col')?.textContent.trim() || '', name: (r.dataset.gameName || '').replace(actionRe, '').trim(), category: CATEGORIES.find(c => c.id === r.dataset.category)?.label || r.dataset.category, details: r.querySelector('td.license_acquisition_col')?.textContent.trim() || '' })); } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = createEl('a', { href: url, download: filename, style: { display: 'none' } }); document.body.appendChild(a); a.click(); setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, CONFIG.BLOB_CLEANUP_MS); } function exportData(fmt) { try { const data = getExportData(); if (!data.length) { alert(t('exportFailed') + ': No data'); return; } const ts = Date.now(); if (fmt === 'csv') { const esc = s => `"${String(s).replace(/"/g, '""')}"`; const csv = ['Date,Name,Category,Details', ...data.map(r => [r.date, r.name, r.category, r.details].map(esc).join(','))].join('\n'); downloadBlob(new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }), `steam-licenses-${ts}.csv`); } else { downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json;charset=utf-8;' }), `steam-licenses-${ts}.json`); } } catch (err) { console.error(`Export ${fmt} failed:`, err); alert(t('exportFailed') + ': ' + err.message); } } // ==================== 高级面板 ==================== function toggleAdvancedPanel() { if (ui.advancedPanel) { const isHidden = ui.advancedPanel.style.display === 'none'; ui.advancedPanel.style.display = isHidden ? '' : 'none'; updateChart(); return; } const panel = document.createElement('div'); panel.id = 'slc-advanced-panel'; panel.innerHTML = `
`; const onDateChange = key => e => { state[key] = e.target.value; state.page = 1; applyPagination(); }; panel.querySelector('#slc-date-from').addEventListener('change', onDateChange('dateFrom')); panel.querySelector('#slc-date-to').addEventListener('change', onDateChange('dateTo')); panel.querySelector('#slc-sort').addEventListener('change', e => { state.sortBy = e.target.value; applyPagination(); }); panel.querySelector('#slc-clear-filter').addEventListener('click', () => { Object.assign(state, { dateFrom: '', dateTo: '', sortBy: 'date-desc', search: '', page: 1, withoutFree: false }); panel.querySelector('#slc-date-from').value = ''; panel.querySelector('#slc-date-to').value = ''; panel.querySelector('#slc-sort').value = 'date-desc'; panel.querySelector('#slc-without-free').checked = false; if (ui.searchInput) ui.searchInput.value = ''; applyPagination(); }); panel.querySelector('#slc-without-free').addEventListener('change', e => { state.withoutFree = e.target.checked; state.page = 1; e.target.blur(); applyPagination(); }); const panelActions = { 'slc-donut-btn': showDonutChart, 'slc-year-btn': showYearChart, 'slc-export-csv': () => exportData('csv'), 'slc-export-json': () => exportData('json') }; for (const [id, fn] of Object.entries(panelActions)) { panel.querySelector(`#${id}`).addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); fn(); }); } const container = document.querySelector(SELECTORS.filterContainer); if (container) container.parentNode.insertBefore(panel, container); ui.advancedPanel = panel; updateChart(); } // ==================== 主界面组装 ==================== function addFilterButtons() { const table = document.querySelector(SELECTORS.table); if (!table || document.querySelector(SELECTORS.filterContainer)) return; const container = createEl('div', { id: 'steam-license-filter' }); // 搜索框 const pageHeader = document.querySelector(SELECTORS.pageHeader); ui.searchInput = createEl('input', { type: 'text', class: 'search-box', placeholder: t('searchPlaceholder'), style: { width: `${CONFIG.SEARCH_WIDTH}px`, marginRight: '125px' } }); let searchTimer = null; ui.searchInput.addEventListener('input', e => { state.search = e.target.value.trim().toLowerCase(); clearTimeout(searchTimer); searchTimer = setTimeout(() => { state.page = 1; applyPagination(); }, CONFIG.DEBOUNCE_MS); }); ui.searchInput.addEventListener('keydown', e => { if (e.key === 'Escape') { ui.searchInput.value = ''; state.search = ''; applyPagination(); } }); ui.searchInput.addEventListener('click', e => e.stopPropagation()); if (pageHeader) { pageHeader.style.position = 'relative'; pageHeader.appendChild(ui.searchInput); const redeemBtn = createEl('button', { class: 'icon-btn', title: `${t('redeemBtn')} (R)`, html: '', style: { position: 'absolute', right: '50px', top: '50%', transform: 'translateY(-50%)' } }); redeemBtn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); showRedeemModal(); }); pageHeader.appendChild(redeemBtn); const advBtn = createEl('button', { class: 'icon-btn', title: `${t('advanced')} (~)`, html: '', style: { position: 'absolute', right: '0', top: '50%', transform: 'translateY(-50%)' } }); advBtn.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); toggleAdvancedPanel(); }); pageHeader.appendChild(advBtn); } // 统计信息 const stats = createEl('div', { id: 'license-stats' }); const totalSpan = createEl('span', {}, [document.createTextNode(`${t('total')}: `), createEl('strong', { style: { display: 'inline-block' } })]); ui.statTotal = totalSpan.querySelector('strong'); stats.appendChild(totalSpan); for (const c of CATEGORIES) { const span = createEl('span', { style: { color: c.color } }, [ document.createTextNode(`${c.label}: `), createEl('strong', { style: { display: 'inline-block' } }) ]); ui.statCounts.set(c.id, span.querySelector('strong')); stats.appendChild(span); } // 图表容器 ui.chartContainer = createEl('div', { style: { width: '100%', marginBottom: '8px', display: 'none' } }); container.appendChild(ui.chartContainer); container.appendChild(stats); container.appendChild(createEl('div', { style: { width: '1px', height: `${CONFIG.SEP_HEIGHT}px`, background: '#4a6a7a', margin: '0 4px' } })); // 筛选按钮 const filterGroup = createEl('div', { style: { display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' } }); const allBtn = createEl('button', { class: 'filter-btn active', 'data-filter': 'all', text: ALL_TYPE.label, style: { '--btn-color': ALL_TYPE.color } }); ui.btns.set('all', allBtn); filterGroup.appendChild(allBtn); for (const cat of CATEGORIES) { const btn = createEl('button', { class: 'filter-btn', 'data-filter': cat.id, text: cat.label, style: { '--btn-color': cat.color } }); ui.btns.set(cat.id, btn); filterGroup.appendChild(btn); } container.appendChild(filterGroup); container.appendChild(createEl('div', { style: { width: '1px', height: `${CONFIG.SEP_HEIGHT}px`, background: '#4a6a7a', margin: '0 4px' } })); // 分页控件 const pagerGroup = createEl('div', { style: { display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap' } }); const { prevBtn, infoWrap, nextBtn, showAllBtn } = createPagerParts(); pagerGroup.append(prevBtn, infoWrap, nextBtn, showAllBtn); container.appendChild(pagerGroup); // 事件委托 container.addEventListener('click', e => { const filterBtn = e.target.closest('button[data-filter]'); if (filterBtn) { ui.btns.forEach(b => b.classList.remove('active')); filterBtn.classList.add('active'); applyFilter(filterBtn.dataset.filter); return; } const actionBtn = e.target.closest('button[data-action]'); if (!actionBtn || actionBtn.disabled) return; const action = actionBtn.dataset.action; if (action === 'prev') { state.page = Math.max(1, state.page - 1); applyPagination(); } else if (action === 'next') { state.page = Math.min(state.totalPages, state.page + 1); applyPagination(); } else if (action === 'showAll') { state.showAll = !state.showAll; if (state.showAll) state.page = 1; applyPagination(); } }); table.parentNode.insertBefore(container, table); // ResizeObserver 动态更新 --filter-h,解决缩放/面板展开时的吸顶错位 const updateFilterH = () => { document.documentElement.style.setProperty('--filter-h', container.offsetHeight + 'px'); }; updateFilterH(); ui.resizeObs = new ResizeObserver(updateFilterH); ui.resizeObs.observe(container); disposers.push(() => { container.remove(); document.documentElement.style.removeProperty('--filter-h'); if (ui.searchInput?.parentNode) ui.searchInput.remove(); ui.resizeObs?.disconnect(); ui.resizeObs = null; }); markRows(); recalcCounts(); lockStatWidths(); applyPagination(); startObserver(); } // ==================== 初始化 ==================== async function init() { for (const d of disposers) { try { d(); } catch(e) { console.warn('disposer error:', e); } } disposers.length = 0; currentLang = detectLanguage(); console.log(`Steam License Classifier v${VERSION} | lang: ${currentLang}`); injectStyles(); updatePageTitle(); addBreadcrumbLink(); try { await waitForElement(`${SELECTORS.tableBody} tr td`); addFilterButtons(); } catch (err) { console.warn('Steam License Classifier init failed:', err.message); } } function injectStyles() { const id = 'steam-license-styles'; if (document.getElementById(id)) return; const style = createEl('style', { id, text: STYLES }); document.head.appendChild(style); disposers.push(() => style.remove()); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function onReady() { document.removeEventListener('DOMContentLoaded', onReady); init(); }); } else { init(); } // BFCache 兼容:pagehide 清理资源,pageshow 恢复时重新初始化 window.addEventListener('pagehide', e => { if (!e.persisted) { for (const d of disposers) { try { d(); } catch(_) {} } disposers.length = 0; observer?.disconnect(); clearTimeout(observerTimer); } }); window.addEventListener('pageshow', e => { if (e.persisted) { for (const d of disposers) { try { d(); } catch(_) {} } disposers.length = 0; init(); } }); // ==================== 快捷键 ==================== const keydownHandler = e => { // 忽略输入框内的按键,以及带修饰键的组合 if (e.target.matches('input, select, textarea') || e.ctrlKey || e.altKey || e.metaKey) { if (e.key === 'Escape' && e.target.matches('input, select, textarea')) e.target.blur(); return; } switch(e.key) { case '/': e.preventDefault(); ui.searchInput?.focus(); break; case '1': applyFilter('all'); break; case '2': applyFilter('store'); break; case '3': applyFilter('retail'); break; case '4': applyFilter('gift'); break; case '5': applyFilter('free'); break; case 'ArrowLeft': if (state.page > 1) { state.page--; applyPagination(); } break; case 'ArrowRight': if (state.page < state.totalPages) { state.page++; applyPagination(); } break; case 'a': case 'A': state.showAll = !state.showAll; if (state.showAll) state.page = 1; applyPagination(); break; case '`': case '~': toggleAdvancedPanel(); break; case 'd': case 'D': showDonutChart(); break; case 'y': case 'Y': showYearChart(); break; case 'r': case 'R': showRedeemModal(); break; } }; document.addEventListener('keydown', keydownHandler); disposers.push(() => document.removeEventListener('keydown', keydownHandler)); // ==================== 动态监听 ==================== let observerTimer = null, observer = null, observerDisposed = false; function startObserver() { if (observer) { observer.disconnect(); observer = null; } clearTimeout(observerTimer); observerTimer = null; const tableBody = getTableBody(); if (!tableBody) return; observer = new MutationObserver(mutations => { const hasNewRow = mutations.some(m => Array.from(m.addedNodes).some(n => n.nodeType === 1 && n.nodeName === 'TR')); if (hasNewRow) { clearTimeout(observerTimer); observerTimer = setTimeout(() => { markRows(); recalcCounts(); lockStatWidths(); applyPagination(); }, CONFIG.OBSERVER_DEBOUNCE_MS); } }); observer.observe(tableBody, { childList: true }); if (!observerDisposed) { observerDisposed = true; disposers.push(() => { observer?.disconnect(); observer = null; clearTimeout(observerTimer); observerTimer = null; }); } } })();