// ==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 = `${escHtml(key)}${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 = `