// ==UserScript==
// @name Steam 许可分类器
// @namespace http://tampermonkey.net/
// @version 2.5.0
// @description 对Steam账户许可进行分类:商店、零售、礼物、免费,支持绝版游戏检查
// @author SmallFork
// @match https://store.steampowered.com/account/licenses*
// @grant GM_info
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @grant GM_getResourceText
// @grant unsafeWindow
// @resource delistedData https://raw.githubusercontent.com/SmallFork/json/main/steam_delisted_apps.json
// @connect api.steampowered.com
// @connect raw.githubusercontent.com
// @license MIT
// @tag Steam
// @tag games
// @homepageURL https://keylol.com/t1035308-1-1
// @icon data:image/svg+xml,
// ==/UserScript==
(function() {
'use strict';
// ==================== 配置 ====================
const CFG = {
PAGE_SIZE: 50, DEBOUNCE: 150, BLOB_CLEANUP: 500,
DATE_W: 160, DAY_MS: 864e5, OBS_DEBOUNCE: 150,
STICKY: 0,
REDEEM_MAX: 9, REDEEM_WAIT: 20,
MAX_PAGES: 3000, PAGE_DELAY: 150,
CACHE_TTL: 7 * 864e5, // 缓存有效期 7 天(毫秒)
INCREMENTAL_MAX_PAGES: 500, // 增量加载兜底:最多抓取页数,防止缓存指纹失配导致死循环
STALE_CACHE_THRESHOLD: 30, // 连续全新页达到此数时判定缓存失效(提高阈值避免大量新增时误判)
STALE_CACHE_RATIO: 0.5 // 全新行占比超过此比例且持续STALE_CACHE_THRESHOLD页时才判定缓存失效
};
// ==================== I18N ====================
const ZH = {
store:'商店',retail:'零售',free:'免费',gift:'礼物',all:'全部',total:'总计',
pageTitle:'{nickname} 的许可',searchPlaceholder:'搜索游戏名称...',
showAll:'显示全部',pagedView:'分页显示',prev:'上一页',next:'下一页',
historyLink:'消费历史记录',
dateFrom:'从',dateTo:'到',
sortBy:'排序',nameAsc:'名称 A-Z',nameDesc:'名称 Z-A',dateLabel:'日期',clearFilter:'清除筛选',
donutTitle:'许可分布详情',
analysisBtn:'许可分析',tabDonut:'许可分布',tabYear:'年份',tabPredict:'许可频率',tabTrend:'增加曲线',tabCal:'日历热力',tabDelisted:'绝版检查',tabSimilar:'相同许可',
exportCSV:'CSV',exportJSON:'JSON',exportFailed:'导出失败',exportNoData:'导出失败: 没有数据',exportSuccess:'导出成功',
csvDate:'日期',csvName:'名称',csvCategory:'分类',csvDetails:'获取方式',
redeemBtn:'激活',redeemTitle:'CDKey 批量激活',
redeemPlaceholder:'输入CDKey,支持批量激活',
activateBtn:'激活Key',pasteBtn:'粘贴',clearBtn:'清空',copySub:'复制Sub',noSubData:'没有可复制的Sub',pasteNoKey:'没提取到Key',pasteKeyCount:'共提取{count}个Key',
indexLabel:'#',resultLabel:'结果',detailLabel:'详情',itemLabel:'项目',
success:'成功',fail:'失败',redeeming:'激活中',waiting:'等待中',
copyFailed:'复制失败Key',noFailedKeys:'没有失败的Key',copiedKeys:'复制成功',clearHistory:'清空历史',
redeemDone:'激活完成:共{total}个,成功{success}个,失败{fail}个',
totalLabel:'总计',successLabel:'成功',failLabel:'失败',
networkError:'网络错误或超时',othersError:'其他错误',
invalidKey:'无效激活码',duplicateKey:'重复激活',rateLimit:'次数上限',regionRestrict:'地区限制',alreadyOwned:'已拥有',missingDep:'缺少主游戏',walletCode:'这是充值码',successDetail:'激活成功',
withoutFree:'排除免费',freeDisabled:'已排除免费,无法查看免费分类',yearTitle:'年份',noData:'暂无数据',
mpTitle:'月度占比',mpMonths:'月份数',mpPeak:'峰值',mpPerMonth:'款/月',mpAdded:'许可',tabMp:'月度占比',backLabel:'返回',
calendarTitle:'日历热力图',calendarNoData:'没有数据',calendarLess:'少',calendarMore:'多',countUnit:'个',
filterCleared:'已清除筛选',clearCache:'清除缓存并刷新',clearCacheConfirm:'确定要清除所有缓存并刷新页面吗?',confirmOk:'确定',confirmCancel:'取消',noKeyInput:'没有输入Key',inputCleared:'已清空',
monthlyDistribution:'月度分布',
similarTitle:'相同许可猜测',similarPrefixLen:'前缀单词数',similarCount:'数量',
dataNote:'数据仅供参考,实际以系统记录为准',
trendTitle:'许可增加曲线',trendCumulative:'累加模式',
predictTitle:'添加许可频率分析',
predictDays:'天',
predictNextDate:'预计下次添加',predictLastDate:'最后添加日期',
predictTotalGames:'添加许可总天数',predictTotalItems:'非免费许可总数',
predictAvgPerDay:'平均每日许可数',predictOverallAvg:'平均间隔',
predictYearAvg:'年均间隔',predictYearCount:'添加天数',
predictYearLicenses:'许可数',predictDailyLicenses:'每日许可数',predictYearRatio:'一年占比',predictNote:'基于历史数据预测,仅供参考',
delistedTitle:'绝版游戏检查',delistedTotal:'绝版总数',delistedOwned:'已拥有',delistedMissing:'未拥有',
delistedOwnership:'绝版拥有率',delistedSearch:'搜索绝版游戏...',delistedFetching:'正在获取游戏库...',
delistedFetchFail:'获取游戏库失败',delistedNoData:'暂无绝版游戏数据',delistedLoaded:'已加载绝版数据',
typeDelisted:'已下架',typePurchaseDisabled:'已禁售',typeF2P:'F2P不可用',typeRetailOnly:'仅零售',typeTestApp:'测试应用',
delistedOwners:'拥有占比',delistedDate:'下架时间',delistedCardName:'游戏名称',delistedCover:'封面',
autoLoadingLicenses:'正在加载入库记录...',
autoLoadingDone:'入库记录加载完成,共 {count} 条',autoLoadingError:'加载入库记录失败: {msg}',
autoLoadingRedirect:'检测到非第一页,正在跳转以加载全部记录...',
loadStatusInit:'正在加载入库记录,请耐心等待...',
loadStatusCache:'正在恢复缓存...',
loadStatusCheck:'正在检查新增 (已对比 {count} 条)...',
loadStatusFetch:'正在加第{page}页... ({loaded}/{total})...',
loadStatusDone:'加载完成',
};
const ZH_TW = { ...ZH, free:'贈品',gift:'禮物',total:'總計',pageTitle:'{nickname} 的授權',
searchPlaceholder:'搜尋遊戲名稱...',showAll:'顯示全部',pagedView:'分頁顯示',
historyLink:'消費歷史記錄',advanced:'進階選項',dateFrom:'從',
nameAsc:'名稱 A-Z',nameDesc:'名稱 Z-A',clearFilter:'清除篩選',
donutTitle:'許可分佈詳情',
analysisBtn:'許可分析',tabDonut:'許可分佈',tabYear:'年份',tabPredict:'許可頻率',tabTrend:'增加曲線',tabCal:'日曆熱力',tabDelisted:'絕版檢查',tabSimilar:'相同許可',
exportFailed:'匯出失敗',exportNoData:'匯出失敗: 沒有資料',exportSuccess:'匯出成功',
csvName:'名稱',csvCategory:'分類',csvDetails:'取得方式',
redeemPlaceholder:'輸入CDKey,支持批量激活',
pasteBtn:'貼上',clearBtn:'清空',copySub:'複製Sub',noSubData:'沒有可複製的Sub',pasteNoKey:'沒提取到Key',pasteKeyCount:'共提取{count}個Key',
indexLabel:'#',resultLabel:'結果',detailLabel:'詳情',itemLabel:'項目',
fail:'失敗',
copyFailed:'複製失敗Key',noFailedKeys:'沒有失敗的Key',copiedKeys:'複製成功',clearHistory:'清空歷史',
redeemDone:'激活完成:共{total}個,成功{success}個,失敗{fail}個',
totalLabel:'總計',successLabel:'成功',failLabel:'失敗',
networkError:'網路錯誤或超時',othersError:'其他錯誤',
invalidKey:'無效激活碼',duplicateKey:'重複激活',rateLimit:'次數上限',regionRestrict:'地區限制',alreadyOwned:'已擁有',missingDep:'缺少主遊戲',walletCode:'這是充值碼',successDetail:'激活成功',
withoutFree:'排除免費',freeDisabled:'已排除免費,無法查看免費分類',yearTitle:'年份',noData:'暫無數據',
mpTitle:'月度佔比',mpMonths:'月份數',mpPeak:'峰值',mpPerMonth:'款/月',mpAdded:'許可',tabMp:'月度佔比',backLabel:'返回',
calendarTitle:'日曆熱力圖',calendarNoData:'沒有數據',calendarLess:'少',calendarMore:'多',countUnit:'個',
filterCleared:'已清除篩選',clearCache:'清除快取並重新整理',clearCacheConfirm:'確定要清除所有快取並重新整理嗎?',confirmOk:'確定',confirmCancel:'取消',noKeyInput:'沒有輸入Key',inputCleared:'已清空',
monthlyDistribution:'月度分布',
similarTitle:'相同許可猜測',similarPrefixLen:'前綴單詞數',similarCount:'數量',
dataNote:'數據僅供參考,實際以系統記錄為準',
trendTitle:'許可增加曲線',trendCumulative:'累加模式',
predictTitle:'添加許可頻率分析',
predictDays:'天',
predictNextDate:'預計下次添加',predictLastDate:'最後添加日期',
predictTotalGames:'添加許可總天數',predictTotalItems:'非免費許可總數',
predictAvgPerDay:'平均每日許可數',predictOverallAvg:'平均間隔',
predictYearAvg:'年均間隔',predictYearCount:'添加天數',
predictYearLicenses:'許可數',predictDailyLicenses:'每日許可數',predictYearRatio:'一年占比',predictNote:'基於歷史數據預測,僅供參考',
delistedTitle:'絕版遊戲檢查',delistedTotal:'絕版總數',delistedOwned:'已擁有',delistedMissing:'未擁有',
delistedOwnership:'絕版擁有率',delistedSearch:'搜尋絕版遊戲...',delistedFetching:'正在獲取遊戲庫...',
delistedFetchFail:'獲取遊戲庫失敗',delistedNoData:'暫無絕版遊戲數據',delistedLoaded:'已載入絕版數據',
typeDelisted:'已下架',typePurchaseDisabled:'已禁售',typeF2P:'F2P不可用',typeRetailOnly:'僅零售',typeTestApp:'測試應用',
delistedOwners:'擁有佔比',delistedDate:'下架時間',delistedCardName:'遊戲名稱',delistedCover:'封面',
autoLoadingLicenses:'正在載入入庫記錄...',
autoLoadingDone:'入庫記錄載入完成,共 {count} 條',autoLoadingError:'載入入庫記錄失敗: {msg}',
autoLoadingRedirect:'檢測到非第一頁,正在跳轉以載入全部記錄...',
loadStatusInit:'正在載入入庫記錄,請耐心等待...',
loadStatusCache:'正在恢復快取...',
loadStatusCheck:'正在檢查新增 (已對比 {count} 條)...',
loadStatusFetch:'正在載入第{page}頁... ({loaded}/{total})...',
loadStatusDone:'載入完成',
};
const EN = { ...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',dateFrom:'From',dateTo:'To',
sortBy:'Sort',nameAsc:'Name A-Z',nameDesc:'Name Z-A',dateLabel:'Date',clearFilter:'Clear Filters',
donutTitle:'License Distribution Detail',
analysisBtn:'Analysis',tabDonut:'Distribution',tabYear:'Year',tabPredict:'Frequency',tabTrend:'Trend',tabCal:'Heatmap',tabDelisted:'Delisted',tabSimilar:'Similar',
exportFailed:'Export Failed',exportNoData:'Export failed: No data',exportSuccess:'Export successful',
csvDate:'Date',csvName:'Name',csvCategory:'Category',csvDetails:'Source',
redeemBtn:'Redeem',redeemTitle:'Batch CDKey Activation',
redeemPlaceholder:'Enter CDKeys, supports batch activation',
activateBtn:'Redeem Keys',pasteBtn:'Paste',clearBtn:'Clear',copySub:'Copy Sub',noSubData:'No Sub data to copy',pasteNoKey:'No keys extracted',pasteKeyCount:'Extracted {count} keys',
indexLabel:'#',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',clearHistory:'Clear History',
redeemDone:'Activation complete: {total} total, {success} succeeded, {fail} failed',
totalLabel:'Total',successLabel:'Success',failLabel:'Failed',
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',successDetail:'Activated',
withoutFree:'Without Free',freeDisabled:'Free excluded, cannot view free category',yearTitle:'Year',noData:'No data',
mpTitle:'Monthly Share',mpMonths:'Months',mpPeak:'Peak',mpPerMonth:'/mo',mpAdded:'added',tabMp:'Monthly',backLabel:'Back',
calendarTitle:'Calendar Heatmap',calendarNoData:'No data',calendarLess:'Less',calendarMore:'More',countUnit:'',
filterCleared:'Filters cleared',clearCache:'Clear cache & reload',clearCacheConfirm:'Are you sure you want to clear all cache and reload?',confirmOk:'Confirm',confirmCancel:'Cancel',noKeyInput:'No keys entered',inputCleared:'Cleared',
monthlyDistribution:'Monthly Distribution',
similarTitle:'Similar License Guess',similarPrefixLen:'Prefix Words',similarCount:'Count',
dataNote:'Data is for reference only, actual records prevail',
trendTitle:'License Growth Trend',trendCumulative:'Cumulative',
predictTitle:'Add License Frequency',
predictDays:'days',
predictNextDate:'Predicted Next',predictLastDate:'Last Added',
predictTotalGames:'Total License Days',predictTotalItems:'Non-free Licenses',
predictAvgPerDay:'Avg Licenses/Day',predictOverallAvg:'Avg Interval',
predictYearAvg:'Year Avg',predictYearCount:'Add Days',
predictYearLicenses:'Licenses',predictDailyLicenses:'Daily Licenses',predictYearRatio:'Year Ratio',predictNote:'Prediction based on historical data, for reference only',
delistedTitle:'Delisted Games Check',delistedTotal:'Total Delisted',delistedOwned:'Owned',delistedMissing:'Missing',
delistedOwnership:'Delisted Ownership',delistedSearch:'Search delisted games...',delistedFetching:'Fetching game library...',
delistedFetchFail:'Failed to fetch game library',delistedNoData:'No delisted game data',delistedLoaded:'Delisted data loaded',
typeDelisted:'Delisted',typePurchaseDisabled:'Purchase Disabled',typeF2P:'F2P Unavailable',typeRetailOnly:'Retail Only',typeTestApp:'Test App',
delistedOwners:'Owner %',delistedDate:'Delisted Date',delistedCardName:'Name',delistedCover:'Cover',
autoLoadingLicenses:'Loading licenses...',
autoLoadingDone:'Loaded {count} licenses',autoLoadingError:'Failed to load licenses: {msg}',
autoLoadingRedirect:'Not on first page, redirecting to load all records...',
loadStatusInit:'Loading licenses, please wait...',
loadStatusCache:'Restoring from cache...',
loadStatusCheck:'Checking for updates (compared {count})...',
loadStatusFetch:'Loading page {page}... ({loaded}/{total})...',
loadStatusDone:'Loading complete',
};
const I18N = { 'zh-CN': ZH, 'zh-TW': ZH_TW, 'zh-HK': ZH_TW, 'zh-MO': ZH_TW, 'en': EN };
let locale = 'zh-CN';
function detectLang() {
const l = (document.documentElement.lang || '').toLowerCase();
if (I18N[l]) return l;
if (l.startsWith('zh') && (l.includes('tw') || l.includes('hant'))) return 'zh-TW';
return l.startsWith('zh') ? 'zh-CN' : 'en';
}
const t = k => I18N[locale]?.[k] ?? ZH[k] ?? k;
const stopEv = (e, fn) => { e.stopPropagation(); e.preventDefault(); fn?.(); };
function toast(msg, type = 'info', ms = 3000) {
const el = document.createElement('div');
el.className = `slc-toast slc-toast--${type}`; el.textContent = msg;
document.body.appendChild(el);
requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('slc-toast--visible')));
const hide = () => {
el.classList.remove('slc-toast--visible');
setTimeout(() => el.remove(), 300);
};
const timer = setTimeout(hide, ms);
el.addEventListener('click', () => { clearTimeout(timer); hide(); });
return { el, hide };
}
function showConfirm(msg) {
return new Promise(resolve => {
const modal = document.createElement('div');
modal.className = 'slc-confirm-modal';
modal.innerHTML = `
${escHtml(msg)}
`;
document.body.appendChild(modal);
requestAnimationFrame(() => requestAnimationFrame(() => modal.classList.add('visible')));
let done = false;
const close = (result) => {
if (done) return; done = true;
modal.classList.remove('visible');
setTimeout(() => modal.remove(), 250);
resolve(result);
};
modal.querySelector('.slc-confirm-ok').addEventListener('click', () => close(true));
modal.querySelector('.slc-confirm-cancel').addEventListener('click', () => close(false));
modal.addEventListener('click', e => { if (e.target === modal) close(false); });
document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { close(false); document.removeEventListener('keydown', esc); } });
});
}
// ==================== SVG 公共属性 ====================
const _S = 'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
// ==================== 分类 ====================
const MATCH_CFG = [
{ id:'store', color:'#3b82f6', icon:``, kw:['Steam 商店','Steam Store'] },
{ id:'retail', color:'#eab308', icon:``, kw:['零售','Retail'] },
{ id:'free', color:'#10b981', icon:``, kw:['免费赠送','贈品','Complimentary'] },
{ id:'gift', color:'#ec4899', icon:``, kw:['礼物','玩家通行证','禮物','招待券','Gift','Guest Pass'] }
];
const CATS = ['store','retail','gift','free'].map(id => {
const c = MATCH_CFG.find(x => x.id === id);
return { id, color: c.color, icon: c.icon, get label() { return t(id); } };
});
const ALL_TYPE = Object.freeze({ id:'all', get label(){ return t('all'); }, color:'#4792c4', icon:`` });
const CAT_RE = new Map(MATCH_CFG.map(c => [
new RegExp(c.kw.map(s => s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('|'),'i'), c.id
]));
const isNoFree = () => state.noFree && state.filter !== 'free';
const chartCounts = () => {
if (!isNoFree()) return state.counts;
const m = new Map(CATS.map(c => [c.id, 0])); let tot = 0;
for (const c of CATS) { if (c.id==='free') continue; const v = state.counts.get(c.id)||0; m.set(c.id,v); tot+=v; }
m.set('total',tot); return m;
};
const chartCats = () => isNoFree() ? CATS.filter(c=>c.id!=='free') : CATS;
const SEL = { tbl:'table.account_table', tb:'table.account_table tbody', hdr:'h2', fc:'#steam-license-filter' };
const VER = GM_info.script.version;
const disposers = [];
const state = {
filter:'all', filterCls:'', search:'', page:1,
showAll:false, totalPages:1,
counts: new Map([...CATS.map(c=>[c.id,0]),['total',0]]),
dateFrom:'', dateTo:'', sortBy:'date-desc', noFree:false,
isProcessing: false, isLoading: false, cachedTb: null, cachedDataRows: null,
timers: { search: null, obs: null },
obs: null
};
const getSessionID = () => { try { return unsafeWindow.g_sessionID || window.g_sessionID || ''; } catch { return window.g_sessionID || ''; } };
// ==================== 样式 ====================
const _rgba = (h, a) => { const v = parseInt(h.slice(1), 16); return `rgba(${(v >> 16) & 255},${(v >> 8) & 255},${v & 255},${a})`; };
const _catCSS = CATS.flatMap(c => [
`table.account_table tbody tr[data-category="${c.id}"]{background:${_rgba(c.color, .15)}}`,
`table.account_table tbody tr[data-category="${c.id}"]:hover{background:${_rgba(c.color, .25)}}`,
`table.account_table tbody.filter-${c.id} tr[data-category]:not([data-category="${c.id}"]){display:none}`
]);
const CSS_CRITICAL = [
':root{--bg-page:#0B1120;--bg-card:#1E293B;--border-card:#334155;--text-primary:#F1F5F9;--text-secondary:#94A3B8;--accent-blue:#3B82F6;--accent-amber:#F59E0B;--accent-pink:#EC4899;--accent-green:#10B981;--accent-purple:#8B5CF6;--radius-md:8px;--radius-lg:12px;--shadow-sm:0 2px 8px rgba(0,0,0,.3);--transition:all .2s ease}',
'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,${CFG.STICKY}px);z-index:99;background:#0B1120;text-transform:none;font-size:13px;font-weight:bold;color:#F1F5F9;padding:12px 12px;border-bottom:1px solid #334155}`,
`table.account_table td.license_date_col{width:${CFG.DATE_W}px!important;min-width:${CFG.DATE_W}px;white-space:nowrap;text-align:left}`,
'table.account_table th.license_date_col{text-align:left}',
'table.account_table td{padding:12px 12px;border-bottom:1px solid rgba(51,65,85,.5);color:#F1F5F9;vertical-align:middle}',
'table.account_table tr:hover td{background:rgba(59,130,246,.08)}',
'#steam-license-filter{margin:0 0 4px;padding:16px 20px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius-lg);backdrop-filter:blur(4px);position:sticky;top:0;z-index:100;contain:layout;display:flex;flex-direction:column;gap:12px}',
`.stats-row{display:flex;gap:10px;flex-wrap:wrap}`,
`.stat-card{background:rgba(15,23,42,.6);border:1px solid var(--border-card);border-radius:var(--radius-md);padding:10px 16px;min-width:100px;text-align:center;cursor:pointer;position:relative;overflow:hidden}`,
`.stat-card:hover{transform:translateY(-2px);border-color:var(--card-color,var(--accent-blue));background:rgba(59,130,246,.1);box-shadow:var(--shadow-sm)}`,
`.stat-card.active{border-color:var(--card-color,var(--accent-blue));background:rgba(59,130,246,.15);box-shadow:var(--shadow-sm)}`,
`.stat-card.active .stat-number,.stat-card.active .stat-label{color:var(--card-color,var(--accent-blue))}`,
`.stat-card.disabled{opacity:.4;cursor:not-allowed}`,
`.stat-number{font-size:1.5rem;font-weight:700;line-height:1.2;color:var(--text-primary)}`,
`.stat-label{font-size:.7rem;letter-spacing:.5px;color:var(--text-secondary);margin-top:2px;display:flex;align-items:center;justify-content:center;gap:3px}`,
`.stat-label svg{width:12px;height:12px;flex-shrink:0}`,
`.stat-bar{height:3px;background:rgba(255,255,255,.08);border-radius:2px;margin-top:6px;overflow:hidden}`,
`.stat-bar-fill{height:100%;border-radius:2px;transition:width .3s ease}`,
`.stat-tools{display:flex;align-items:center;gap:4px;flex-wrap:wrap}`,
`.tool-btn{background:rgba(15,23,42,.6)!important;border:1px solid var(--border-card)!important;border-radius:var(--radius-md)!important;padding:0 10px!important;font-size:13px!important;height:34px!important;width:auto!important;color:var(--text-secondary)!important;cursor:pointer;transition:var(--transition);display:flex;align-items:center;justify-content:center;box-sizing:border-box}`,
`.tool-btn:hover{background:rgba(59,130,246,.2)!important;color:var(--text-primary)!important;border-color:var(--accent-blue)!important;transform:translateY(-1px)}`,
`.toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:10px}`,
`.toolbar .search-box{flex:2;min-width:180px;background:#0F172A;border:1px solid var(--border-card);border-radius:40px;padding:8px 16px;color:#F1F5F9;font-size:13px;outline:none;transition:var(--transition)}`,
`.toolbar .search-box::placeholder{color:var(--text-secondary)}`,
`.toolbar .search-box:focus{border-color:var(--accent-blue);box-shadow:0 0 0 2px rgba(59,130,246,.3)}`,
`.adv-group{display:flex;align-items:center;gap:6px;flex-wrap:wrap}`,
`.adv-lbl{color:var(--text-secondary);font-size:12px;white-space:nowrap}`,
`.adv-date{position:relative;background:#0F172A!important;border:1px solid var(--border-card);border-radius:40px;color:#F1F5F9!important;padding:8px 16px;font-size:13px;outline:none;transition:var(--transition);color-scheme:dark;height:36px;width:140px;-webkit-appearance:none;appearance:none;box-sizing:border-box;line-height:normal;text-align:center}`,
`.adv-date::-webkit-calendar-picker-indicator{opacity:0;cursor:pointer;width:20px;height:20px}`,
`.adv-date::after{content:'';position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;pointer-events:none;background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E") no-repeat center}`,
`.adv-date::-webkit-datetime-edit,.adv-date::-webkit-datetime-edit-fields-wrapper,.adv-date::-webkit-datetime-edit-text,.adv-date::-webkit-datetime-edit-month-field,.adv-date::-webkit-datetime-edit-day-field,.adv-date::-webkit-datetime-edit-year-field{background:transparent;color:#F1F5F9}`,
`.adv-date:focus{border-color:var(--accent-blue);box-shadow:0 0 0 2px rgba(59,130,246,.3)}`,
`.adv-sort{background:#0F172A;border:1px solid var(--border-card);border-radius:40px;color:#F1F5F9;padding:8px 16px;font-size:13px;outline:none;transition:var(--transition);color-scheme:dark;height:36px;-webkit-appearance:none;appearance:none;box-sizing:border-box;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:32px;cursor:pointer}`,
`.adv-sort option{background:#0B1120;color:var(--text-primary)}`,
`.adv-sort:focus{border-color:var(--accent-blue);box-shadow:0 0 0 2px rgba(59,130,246,.3)}`,
`.adv-clr{background:rgba(15,23,42,.6)!important;border:1px solid var(--border-card)!important;border-radius:40px!important;padding:0 10px!important;font-size:13px!important;height:36px!important;width:auto!important;color:var(--text-secondary)!important;cursor:pointer;transition:var(--transition);display:flex;align-items:center;justify-content:center;box-sizing:border-box}`,
`.adv-clr:hover{background:rgba(59,130,246,.2)!important;color:var(--text-primary)!important;border-color:var(--accent-blue)!important;transform:translateY(-1px)}`,
`.pager{display:flex;align-items:center;gap:6px;margin-left:auto}`,
`#steam-license-filter button.pager-btn{background:rgba(15,23,42,.6);border:1px solid var(--border-card);border-radius:var(--radius-md);padding:6px 14px;font-size:13px;height:auto;color:var(--text-primary);cursor:pointer;transition:var(--transition)}`,
`#steam-license-filter button.pager-btn:hover{background:rgba(59,130,246,.2);transform:translateY(-1px)}`,
`#steam-license-filter button.pager-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}`,
`.page-info{display:inline-flex;align-items:center;gap:2px;font-size:13px;font-weight:500;white-space:nowrap;color:var(--text-secondary)}`,
`.page-input{width:30px;text-align:center;background:rgba(0,0,0,.3);border:1px solid #4a6a7a;border-radius:4px;color:#c7d5e0;font-size:13px;padding:2px 4px;outline:none}`,
`.page-input:focus{border-color:var(--accent-blue)}`,
`#steam-license-filter button{border:2px solid transparent;border-radius:6px;background:transparent;color:var(--text-primary);cursor:pointer;font-weight:500;transition:var(--transition);outline:none;will-change:transform}`,
`#steam-license-filter button:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}`,
..._catCSS,
'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{display:none}',
'#slc-no-free{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer;user-select:none;font-size:12px;color:var(--text-secondary);white-space:nowrap}',
'#slc-no-free input[type="checkbox"]{position:absolute;opacity:0;width:0;height:0}',
'#slc-no-free .tg-track{position:relative;width:44px;height:20px;background:#1E293B;border:1px solid var(--border-card);border-radius:10px;cursor:pointer;transition:var(--transition);flex-shrink:0}',
'#slc-no-free .tg-track::after{content:"";position:absolute;left:2px;top:2px;width:14px;height:14px;background:var(--text-secondary);border-radius:50%;transition:var(--transition)}',
'#slc-no-free input:checked+.tg-track{background:rgba(139,92,246,.3);border-color:var(--accent-purple)}',
'#slc-no-free input:checked+.tg-track::after{left:26px;background:var(--accent-purple)}',
'.stats[class*="svelte-"]{display:none!important}',
'#license-stats{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:rgba(59,130,246,.2)}',
'#steam-license-filter button[data-action]:disabled{opacity:.4;cursor:not-allowed}',
'#steam-license-filter button[data-action].active-mode{background:var(--accent-purple);color:#fff}',
'table.account_table tbody.search-filtered tr[data-category]:not(.search-match){display:none}',
`@media(max-width:899px){.stat-card{min-width:80px;padding:8px 10px}.stat-number{font-size:1.2rem}.toolbar .search-box{min-width:140px}.stat-tools{gap:3px}.tool-btn{padding:0 6px!important;height:30px!important}.adv-date{width:115px;padding:6px 10px;font-size:12px;height:32px}}`,
`@media(max-width:599px){.stat-card{min-width:70px;padding:6px 8px}.stat-number{font-size:1rem}.pager-btn{padding:4px 8px!important;font-size:.75rem!important}.stat-tools{gap:2px}.tool-btn svg{width:14px;height:14px}.tool-btn{height:26px!important}#slc-no-free{font-size:11px}.adv-date{width:105px;height:30px;padding:6px 8px;font-size:12px}.adv-sort{font-size:12px;height:30px;padding:6px 10px}}`,
'.slc-toast{position:fixed;bottom:20px;right:20px;padding:12px 20px;border-radius:8px;font-size:14px;font-weight:500;z-index:10001;opacity:0;transform:translateY(10px);transition:all .3s ease;pointer-events:auto;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,.4)}',
'.slc-toast--visible{opacity:1;transform:translateY(0)}',
'.slc-toast--info{background:#2a475e;color:#c7d5e0;border:1px solid #4a6a7a}',
'.slc-toast--success{background:rgba(16,185,129,.9);color:#fff}',
'.slc-toast--error{background:rgba(239,68,68,.9);color:#fff}',
'.slc-toast--warning{background:rgba(245,158,11,.9);color:#fff}',
'.slc-load-bar{width:100%;height:4px;background:rgba(255,255,255,.08);overflow:hidden;border-radius:2px;margin:0 0 4px}',
'.slc-load-bar.hidden{opacity:0;transition:opacity .3s ease}',
'.slc-load-bar-fill{height:100%;width:0;background:linear-gradient(90deg,var(--accent-blue),var(--accent-purple));border-radius:2px;transition:width .3s ease}',
'.slc-load-status{display:inline;margin-left:8px;font-size:13px;color:var(--text-secondary);font-weight:400;white-space:nowrap;animation:slc-pulse 1.5s ease-in-out infinite}',
'@keyframes slc-pulse{0%,100%{opacity:.6}50%{opacity:1}}'
].join('');
const CSS_DEFERRED = [
'.slc-modal{position:fixed;inset:0;background:rgba(11,17,32,.85);backdrop-filter:blur(4px);z-index:10000;display:flex;opacity:0;visibility:hidden}',
'#slc-analysis-modal,#slc-redeem-modal{align-items:center;justify-content:center}',
'.slc-modal.visible{opacity:1;visibility:visible}',
'.slc-mc{background:rgba(30,41,59,.95);backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.1);border-radius:16px;box-shadow:0 8px 32px rgba(0,0,0,.5);width:90%;position:relative;transform:scale(.95);opacity:0;transition:transform .3s ease,opacity .3s ease}',
'.slc-modal.visible .slc-mc{transform:scale(1);opacity:1}',
'#slc-analysis-c{width:1100px;max-width:96vw;height:720px;max-height:92vh;contain:layout}',
'#slc-redeem-c{width:1100px;max-width:96vw;height:720px;max-height:92vh;display:flex;flex-direction:column;contain:layout}',
/* ===== 窗口标题栏(学习好友管理器家庭组窗口) ===== */
'.slc-win{padding:0!important;display:flex;flex-direction:column;overflow:hidden}',
'.slc-win-header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0;user-select:none;padding:10px 16px;border-radius:15px 15px 0 0}',
'.slc-win-title{margin:0;color:#fff;font-weight:700;font-size:14px;display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden;white-space:nowrap}',
'.slc-win-title .slc-win-ico{display:inline-flex;flex-shrink:0}',
'.slc-win-title .slc-win-ico svg{width:18px;height:18px;stroke:currentColor;fill:none}',
'.slc-win-sub{font-size:12px;font-weight:400;color:#8a9ba8;flex-shrink:0}',
'.slc-win-actions{display:flex;align-items:center;gap:4px;flex-shrink:0;margin-left:12px}',
'.slc-win-close{background:rgba(255,255,255,.06);border:none;color:#94a3b8;cursor:pointer;width:28px;height:28px;border-radius:6px;display:flex;align-items:center;justify-content:center;transition:all .2s}',
'.slc-win-close:hover{background:rgba(244,63,94,.2);color:#fb7185}',
'.slc-win-close svg{width:14px;height:14px}',
'.slc-win-body{flex:1;min-height:0;display:flex;flex-direction:column;padding:14px 20px 16px}',
'.slc-mf{margin-top:16px;padding-top:14px;border-top:1px solid #334155;display:flex;align-items:flex-start;gap:10px;flex-shrink:0}',
'.slc-mf svg{width:16px;height:16px;stroke:#64748b;fill:none;flex-shrink:0;margin-top:1px}',
'.slc-mf-t{font-size:13px;color:#64748b;line-height:1.5}',
/* ===== 统一分析窗口:顶部标签栏(仿好友管理器家庭组窗口) ===== */
'.slc-tabs{display:flex;gap:2px;background:rgba(15,23,42,.6);padding:3px;border-radius:8px;border:1px solid rgba(255,255,255,.08);margin-bottom:14px;flex-shrink:0}',
'.slc-tab{flex:1;border:none;background:transparent;color:var(--text-secondary);padding:7px 4px;font-size:12px;font-weight:600;border-radius:6px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:5px;white-space:nowrap;overflow:hidden}',
'.slc-tab svg{width:14px;height:14px;stroke:currentColor;fill:none;flex-shrink:0}',
'.slc-tab:hover{color:#fff}',
'.slc-tab.active{color:var(--c,#06cfbe);background:var(--cb,rgba(6,207,190,.15))}',
'.slc-panels{flex:1;min-height:0;display:flex;flex-direction:column}',
'.slc-panel{display:none;flex:1;min-height:0;flex-direction:column;overflow:hidden}',
'.slc-panel.active{display:flex}',
'.slc-pbody{flex:1;min-height:0;overflow-y:auto}',
'.slc-panel>.slc-cal-legend{flex:0 0 auto;margin-top:16px}',
'.slc-cal-container{display:flex;flex-direction:column;gap:24px}',
'.slc-cal-year{display:flex;flex-direction:column;gap:8px}',
'.slc-cal-year-label{font-size:16px;font-weight:600;color:#f8fafc}',
'.slc-cal-grid{display:flex;flex-direction:column;gap:4px}',
'.slc-cal-months{display:flex;gap:4px;padding-left:32px}',
'.slc-cal-months span{flex:1;text-align:center;font-size:11px;color:#64748b}',
'.slc-cal-days{display:flex;gap:4px}',
'.slc-cal-weekdays{display:flex;flex-direction:column;gap:4px;width:24px}',
'.slc-cal-weekdays span{font-size:10px;color:#64748b;text-align:center;height:14px;line-height:14px}',
'.slc-cal-cells{display:grid;grid-template-columns:repeat(53,1fr);grid-template-rows:repeat(7,1fr);grid-auto-flow:column;gap:2px;flex:1}',
'.slc-cal-cell{width:14px;height:14px;border-radius:2px;transition:all .15s}',
'.slc-cal-cell:hover{transform:scale(1.2);z-index:1}',
'.slc-cal-empty{background:transparent}',
'.slc-cal-legend{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:16px;padding:16px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px}',
'.slc-cal-legend-label{font-size:12px;color:#64748b}',
'.slc-cal-legend-cells{display:flex;gap:3px}',
'.slc-cal-legend-cells div{width:14px;height:14px;border-radius:2px}',
/* ===== 通用:卡片内 SVG 自适应填充 ===== */
'.slc-svg-fill{flex:1;min-height:0;display:flex;align-items:center;justify-content:center}',
'.slc-sec-title{font-size:12px;font-weight:700;color:#c7d5e0;display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-shrink:0}',
'.slc-sec-title .dot{width:8px;height:8px;border-radius:2px;display:inline-block;flex-shrink:0}',
/* ===== 许可分布 tab:年份柱状图(左) + 饼图/柱状图(右上/右下) ===== */
'#slc-donut-body{display:flex;flex-direction:column;overflow:hidden}',
'#slc-dt-wrap{flex:1;min-height:0;display:flex;gap:12px}',
'#slc-dt-left{flex:7;min-width:0;display:flex;flex-direction:column;padding:12px 14px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px}',
'.slc-dt-head{display:flex;align-items:center;justify-content:space-between;flex-shrink:0;margin-bottom:8px}',
'.slc-dt-head .slc-sec-title{margin-bottom:0}',
'.slc-dt-back{display:none;align-items:center;gap:4px;padding:3px 10px;background:rgba(102,192,244,.12);border:1px solid rgba(102,192,244,.4);border-radius:6px;color:#66c0f4;font-size:11px;font-weight:600;cursor:pointer;transition:all .2s}',
'.slc-dt-back.show{display:inline-flex}',
'.slc-dt-back:hover{background:rgba(102,192,244,.25)}',
'#slc-dt-right{flex:4;min-width:0;min-height:0;display:flex;flex-direction:column;gap:12px}',
'#slc-dt-pie{flex:1;min-height:0;display:flex;flex-direction:column;padding:12px 14px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px;overflow:hidden}',
'#slc-dt-pie svg{max-width:100%;max-height:100%}',
'#slc-dt-bars{flex:1;min-height:0;display:flex;flex-direction:column;padding:12px 14px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px;overflow:hidden}',
'#slc-dt-bars .bar-chart{flex:1;min-height:0;display:flex;align-items:stretch;justify-content:space-around;gap:10px}',
'#slc-dt-bars .bar-col{display:flex;flex-direction:column;align-items:center;width:52px;flex:1;min-height:0;justify-content:flex-end}',
'#slc-dt-bars .bar-track-v{width:100%;flex:1;background:rgba(255,255,255,.06);border-radius:6px 6px 0 0;position:relative;display:flex;flex-direction:column;justify-content:flex-end;min-height:0}',
'#slc-dt-bars .bar-fill-v{width:100%;border-radius:6px 6px 0 0;transition:height .3s ease}',
'#slc-dt-bars .bar-value{font-size:12px;color:#c7d5e0;margin-bottom:3px;font-weight:600}',
'#slc-dt-bars .bar-label{font-size:11px;color:#94a3b8;margin-top:4px;text-align:center;white-space:nowrap}',
'.slc-donut-legend-c{display:flex;flex-wrap:wrap;justify-content:center;gap:4px 12px;padding-top:8px;flex-shrink:0}',
'.slc-donut-legend-c .li{display:flex;align-items:center;gap:5px;font-size:11px;color:#94a3b8;white-space:nowrap}',
'.slc-donut-legend-c .lc{width:9px;height:9px;border-radius:2px;flex-shrink:0}',
'.slc-donut-legend-c .ll{color:#c7d5e0}',
'.slc-donut-legend-c .lv{color:#8a9ba8}',
/* ===== 月度占比 tab(学习好友管理器家庭组月度饼图) ===== */
'#slc-mp-body{display:flex;flex-direction:column;overflow:hidden}',
'.slc-mp-wrap{flex:1;min-height:0;display:flex;flex-direction:column;padding:12px 14px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px;overflow:hidden}',
'.slc-mp-legend{display:flex;flex-wrap:wrap;justify-content:center;gap:4px 12px;padding:2px 0 8px;border-bottom:1px solid rgba(255,255,255,.06);flex-shrink:0}',
'.slc-mp-scroll{flex:1;overflow-y:auto;min-height:0;padding:4px 2px}',
'.slc-mp-year-group{margin-bottom:10px}',
'.slc-mp-year-title{font-size:12px;font-weight:700;color:#a78bfa;margin-bottom:5px;padding-left:8px;border-left:3px solid #8b5cf6;display:flex;align-items:center;gap:6px}',
'.slc-mp-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:5px}',
'.slc-mp-item{display:flex;flex-direction:column;align-items:center;gap:2px;background:rgba(15,23,42,.35);border:1px solid rgba(255,255,255,.04);border-radius:8px;padding:5px 2px 4px;transition:all .2s;min-width:0}',
'.slc-mp-item:hover{background:rgba(255,255,255,.04);border-color:rgba(6,207,190,.25);transform:translateY(-1px)}',
'.slc-mp-item.empty{background:rgba(15,23,42,.15);border-color:rgba(255,255,255,.02);opacity:.5}',
'.slc-mp-svg-wrap{width:100%;aspect-ratio:1;margin:0 auto;display:flex;align-items:center;justify-content:center}',
'.slc-mp-label{font-size:10px;font-weight:700;color:#e2e8f0;line-height:1.2}',
'.slc-mp-summary{display:flex;justify-content:space-around;font-size:11px;color:#94a3b8;padding:6px 0 0;border-top:1px solid rgba(255,255,255,.06);flex-shrink:0}',
/* ===== 增加曲线 tab:图表卡片盛满 ===== */
'#slc-trend-body{display:flex;flex-direction:column;overflow:hidden}',
'.slc-trend-chart{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;padding:12px 14px;background:var(--bg-card);border:1px solid var(--border-card);border-radius:12px}',
'#slc-year-legend{display:flex;gap:16px;justify-content:center;margin-top:12px;flex-wrap:wrap;padding:16px;background:#1e293b;border:1px solid #334155;border-radius:12px}',
'#slc-year-legend .yl{display:flex;align-items:center;gap:6px;font-size:13px;color:#c7d5e0}',
'#slc-year-legend .yc{width:12px;height:12px;border-radius:2px;flex-shrink:0}',
'#slc-redeem-c .ra{display:flex;gap:8px;margin-top:16px;align-items:center;flex-wrap:wrap;justify-content:flex-end}',
'#slc-rcf{margin-left:auto}',
'#slc-rstats{display:flex;gap:8px;flex:1;min-width:0}',
'#slc-rstats .pc{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:8px;min-width:90px}',
'#slc-rstats .pc-i{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0}',
'#slc-rstats .pc-i svg{width:14px;height:14px}',
'#slc-rstats .pc-i.blue{background:rgba(59,130,246,.15)}',
'#slc-rstats .pc-i.green{background:rgba(34,197,94,.15)}',
'#slc-rstats .pc-i.red{background:rgba(239,68,68,.15)}',
'#slc-rstats .pc-t{display:flex;align-items:center;gap:6px;min-width:0;flex:1}',
'#slc-rstats .pc-l{font-size:10px;color:#64748b;letter-spacing:.3px}',
'#slc-rstats .pc-v{font-size:15px;font-weight:700;color:#f8fafc}',
'#slc-rt,#slc-pt{width:100%;border-collapse:collapse;font-size:13px;background:#1e293b;border:1px solid #334155;overflow:hidden}',
'#slc-rt{margin-top:12px;border-radius:12px;flex:1;min-height:0;overflow-y:auto;display:block}',
'#slc-rt thead th{position:sticky;top:0;z-index:1;background:#16202e}',
'#slc-redeem-c .slc-mf{margin-top:auto}',
'#slc-pt{border-radius:10px}',
'#slc-rt th,#slc-pt 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-rt td,#slc-pt td{padding:7px 10px;border-bottom:1px solid rgba(255,255,255,.05);color:#c7d5e0;vertical-align:middle}',
'#slc-rt tr:hover td,#slc-pt tr:hover td{background:rgba(255,255,255,.03)}',
'#slc-rt code{color:#66c0f4;font-size:13px;background:rgba(0,0,0,.2);padding:1px 5px;border-radius:3px}',
'#slc-rt a{color:#66c0f4;text-decoration:none;transition:color .15s}',
'#slc-rt a:hover{color:#fff}',
'#slc-ps{display:flex;flex-direction:column;gap:8px;min-width:160px}',
'#slc-ps .pc{background:#1e293b;border:1px solid #334155;border-radius:10px;padding:14px;display:flex;align-items:center;gap:12px}',
'#slc-ps .pc-i{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0}',
'#slc-ps .pc-i svg{width:20px;height:20px;stroke:#fff;fill:none}',
'#slc-ps .pc-i.blue{background:rgba(59,130,246,.15)}#slc-ps .pc-i.blue svg{stroke:#3b82f6}',
'#slc-ps .pc-i.purple{background:rgba(139,92,246,.15)}#slc-ps .pc-i.purple svg{stroke:#8b5cf6}',
'#slc-ps .pc-i.cyan{background:rgba(6,182,212,.15)}#slc-ps .pc-i.cyan svg{stroke:#06b6d4}',
'#slc-ps .pc-i.green{background:rgba(34,197,94,.15)}#slc-ps .pc-i.green svg{stroke:#22c55e}',
'#slc-ps .pc-i.orange{background:rgba(245,158,11,.15)}#slc-ps .pc-i.orange svg{stroke:#f59e0b}',
'#slc-ps .pc-i.rose{background:rgba(236,72,153,.15)}#slc-ps .pc-i.rose svg{stroke:#ec4899}',
'#slc-ps .pc-t{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}',
'#slc-ps .pc-l{font-size:11px;color:#64748b;text-transform:uppercase;letter-spacing:.3px}',
'#slc-ps .pc-v{font-size:18px;font-weight:700;color:#f8fafc}',
'#slc-ps .pc-v.ac{color:#ec4899}',
'#slc-pb{display:flex;gap:20px;align-items:flex-start}',
'#slc-pr{flex:1;min-width:0}',
'#slc-pt .pb{height:5px;background:#334155;border-radius:2px;margin-top:3px;overflow:hidden}',
'#slc-pt .pb-f{height:100%;background:#f59e0b;border-radius:2px;transition:width .3s ease}',
'#slc-redeem-c textarea,#slc-redeem-c 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-c textarea:focus,#slc-redeem-c 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-c button.rb{border:none;border-radius:2px;padding:8px 20px;font-size:13px;font-weight:400;cursor:pointer;transition:all .15s ease;outline:none;text-transform:uppercase;letter-spacing:.5px}',
'#slc-redeem-c button.rb:hover{filter:brightness(1.2)}',
'#slc-redeem-c button.rb:active{filter:brightness(.9)}',
'#slc-redeem-c button.rb:disabled{opacity:.4;cursor:not-allowed;filter:none}',
'#slc-redeem-c button.rb1{background:linear-gradient(to right,#47bfff 5%,#1a44c2 60%);color:#fff}',
'#slc-redeem-c button.rb1:hover:not(:disabled){background:linear-gradient(to right,#5ccfff 5%,#2555d5 60%)}',
'#slc-redeem-c button.rb2{background:rgba(0,0,0,.4);color:#8f98a0;border:1px solid rgba(255,255,255,.08)}',
'#slc-redeem-c button.rb2:hover:not(:disabled){background:rgba(0,0,0,.6);color:#c6d4df;border-color:rgba(255,255,255,.15)}',
'.slc-edd{position:relative;display:inline-flex;z-index:999999}',
'.slc-edd .em{display:none;position:absolute;top:100%;right:0;margin-top:4px;background:#0F172A;border:1px solid var(--border-card);border-radius:8px;overflow:hidden;z-index:99999;box-shadow:0 4px 12px rgba(0,0,0,.4);min-width:100px}',
'.slc-edd.open .em{display:block}',
'.slc-edd .em button{display:block;width:100%;padding:8px 16px;background:none;border:none;color:#F1F5F9;font-size:13px;cursor:pointer;text-align:left;white-space:nowrap;transition:background .15s}',
'.slc-edd .em button:hover{background:rgba(59,130,246,.2);color:var(--accent-blue)}',
/* ===== 绝版游戏检查窗口 ===== */
'#slc-delisted-body{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}',
'.slc-delisted-header{flex-shrink:0;padding-bottom:8px}',
'.slc-delisted-scroll{flex:1;overflow-y:auto;min-height:0;padding-right:4px;overscroll-behavior:auto}',
'.slc-delisted-kpi-row{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}',
'.slc-delisted-kpi-card{flex:1;min-width:100px;background:rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:10px 14px;text-align:center;transition:all .2s}',
'.slc-delisted-kpi-card:hover{background:rgba(255,255,255,.06)}',
'.slc-delisted-kpi-card.active{background:rgba(255,255,255,.1);box-shadow:0 0 0 2px rgba(255,255,255,.15) inset}',
'.slc-delisted-kpi-value{font-size:1.3rem;font-weight:700;color:#f8fafc}',
'.slc-delisted-kpi-label{font-size:11px;color:#64748b;margin-top:2px}',
'.slc-delisted-progress{height:5px;background:#334155;border-radius:2px;overflow:hidden;margin-bottom:12px}',
'.slc-delisted-progress-fill{height:100%;background:linear-gradient(90deg,#22c55e,#66c0f4);border-radius:2px;transition:width .3s ease}',
'.slc-delisted-type-row{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}',
'.slc-delisted-type-btn{font-size:11px;padding:4px 10px;border-radius:6px;cursor:pointer;transition:all .2s;font-weight:500;white-space:nowrap}',
'.slc-delisted-type-btn:hover{transform:translateY(-1px);filter:brightness(1.2)}',
'.slc-delisted-type-btn.active{font-weight:700;box-shadow:0 2px 6px rgba(0,0,0,.3)}',
'.slc-delisted-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px}',
'.slc-delisted-toolbar input[type=text]{flex:1;min-width:160px;background:rgba(0,0,0,.3);border:1px solid #4a6a7a;border-radius:20px;padding:6px 14px;color:#c7d5e0;font-size:12px;outline:none;transition:all .2s}',
'.slc-delisted-toolbar input[type=text]:focus{border-color:#66c0f4;box-shadow:0 0 0 2px rgba(102,192,244,.2)}',
'.slc-delisted-toolbar button,.slc-delisted-pagination button{background:rgba(15,23,42,.6)!important;border:1px solid #334155!important;border-radius:6px!important;padding:4px 10px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s}',
'.slc-delisted-toolbar button:hover,.slc-delisted-pagination button:hover{background:rgba(59,130,246,.2)!important;color:#f1f5f9!important}',
'.slc-delisted-toolbar button.active{background:rgba(102,192,244,.2)!important;border-color:rgba(102,192,244,.4)!important;color:#66c0f4!important}',
'.slc-delisted-toolbar .slc-dlst-sort-group{display:flex;gap:0;border:1px solid #334155;border-radius:6px;overflow:hidden}',
'.slc-delisted-toolbar .slc-dlst-sort-btn{background:rgba(15,23,42,.6)!important;border:none!important;border-right:1px solid #334155!important;border-radius:0!important;padding:4px 10px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s}',
'.slc-delisted-toolbar .slc-dlst-sort-btn:last-child{border-right:none!important}',
'.slc-delisted-toolbar .slc-dlst-sort-btn:hover{background:rgba(59,130,246,.2)!important;color:#f1f5f9!important}',
'.slc-delisted-toolbar .slc-dlst-sort-btn.active{background:rgba(102,192,244,.2)!important;color:#66c0f4!important}',
'.slc-delisted-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px}',
'.slc-delisted-card{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:10px 14px;transition:background .2s,border-color .2s;cursor:pointer;position:relative}',
'.slc-delisted-card:hover{background:rgba(255,255,255,.08);border-color:rgba(244,67,54,.3)}',
'.slc-delisted-card-name{font-size:13px;font-weight:600;color:#f1f5f9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}',
'.slc-delisted-card-meta{font-size:11px;color:#64748b;display:flex;align-items:center;gap:6px;flex-wrap:wrap}',
'.slc-delisted-badge{display:inline-block;font-size:10px;font-weight:600;padding:1px 6px;border-radius:3px}',
'.slc-delisted-badge.type-delisted{background:rgba(244,67,54,.2);color:#f44336}',
'.slc-delisted-badge.type-purchase_disabled{background:rgba(255,152,0,.2);color:#ff9800}',
'.slc-delisted-badge.type-f2p_unavailable{background:rgba(156,39,176,.2);color:#ce93d8}',
'.slc-delisted-badge.type-retail_only{background:rgba(33,150,243,.2);color:#64b5f6}',
'.slc-delisted-badge.type-test_app{background:rgba(38,198,218,.2);color:#26c6da}',
'.slc-delisted-card .owned-badge{position:absolute;bottom:6px;right:6px;font-size:10px;padding:2px 6px;border-radius:3px;font-weight:600}',
'.slc-delisted-card .owned-badge.owned{background:rgba(34,197,94,.2);color:#22c55e}',
'.slc-delisted-card .owned-badge.missing{background:rgba(244,67,54,.15);color:#f44336}',
'.slc-delisted-card-achievements,.slc-delisted-card-owners{font-size:10px}',
'.slc-delisted-card-achievements{color:#66c0f4}',
'.slc-delisted-card-owners{color:#94a3b8}',
'.slc-delisted-list{display:flex;flex-direction:column;gap:4px}',
'.slc-delisted-list-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.05);border-radius:6px;cursor:pointer;transition:background .2s,border-color .2s}',
'.slc-delisted-list-item:hover{background:rgba(255,255,255,.06);border-color:rgba(244,67,54,.2)}',
'.slc-delisted-list-item .name{flex:1;font-size:12px;color:#f1f5f9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
'.slc-delisted-list-item .meta{display:flex;align-items:center;gap:6px;flex-shrink:0}',
'.slc-delisted-list-item.has-cover{gap:12px}',
'.slc-delisted-cover{width:120px;height:45px;border-radius:4px;object-fit:cover;flex-shrink:0;background:rgba(255,255,255,.05)}',
'.slc-delisted-cover-btn{background:rgba(15,23,42,.6)!important;border:1px solid #334155!important;border-radius:6px!important;padding:4px 10px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s}',
'.slc-delisted-cover-btn:hover{background:rgba(59,130,246,.2)!important;color:#f1f5f9!important}',
'.slc-delisted-cover-btn.active{background:rgba(102,192,244,.2)!important;border-color:rgba(102,192,244,.4)!important;color:#66c0f4!important}',
'.slc-delisted-pagination{display:flex;align-items:center;gap:6px;justify-content:center}',
'.slc-delisted-pagination button:disabled{opacity:.4;cursor:not-allowed}',
'.slc-delisted-pagination .page-info{font-size:12px;color:#64748b}',
'.slc-delisted-loading{text-align:center;padding:40px;color:#64748b;font-size:14px}',
'.slc-delisted-loading .spinner{display:inline-block;width:24px;height:24px;border:3px solid rgba(102,192,244,.2);border-top-color:#66c0f4;border-radius:50%;animation:slc-spin .8s linear infinite;margin-bottom:10px}',
'@keyframes slc-spin{to{transform:rotate(360deg)}}',
/* ===== 确认弹窗 ===== */
'.slc-confirm-modal{position:fixed;inset:0;background:rgba(11,17,32,.85);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:10001;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:opacity .25s ease}',
'.slc-confirm-modal.visible{opacity:1;visibility:visible}',
'.slc-confirm-card{background:var(--bg-card);border:1px solid var(--border-card);border-radius:var(--radius-lg);padding:28px 32px 24px;width:90%;max-width:380px;box-shadow:0 8px 32px rgba(0,0,0,.5);transform:scale(.92);opacity:0;transition:transform .25s ease,opacity .25s ease;text-align:center}',
'.slc-confirm-modal.visible .slc-confirm-card{transform:scale(1);opacity:1}',
'.slc-confirm-icon{width:52px;height:52px;margin:0 auto 16px;border-radius:50%;background:rgba(239,68,68,.15);display:flex;align-items:center;justify-content:center}',
'.slc-confirm-icon svg{width:26px;height:26px;stroke:#ef4444;fill:none}',
'.slc-confirm-msg{font-size:14px;color:var(--text-primary);line-height:1.6;margin-bottom:24px}',
'.slc-confirm-btns{display:flex;gap:10px;justify-content:center}',
'.slc-confirm-cancel{flex:1;padding:9px 0;border-radius:var(--radius-md);font-size:13px;font-weight:500;cursor:pointer;transition:var(--transition);background:rgba(255,255,255,.06);border:1px solid var(--border-card);color:var(--text-secondary)}',
'.slc-confirm-cancel:hover{background:rgba(255,255,255,.12);color:var(--text-primary)}',
'.slc-confirm-ok{flex:1;padding:9px 0;border-radius:var(--radius-md);font-size:13px;font-weight:600;cursor:pointer;transition:var(--transition);background:rgba(239,68,68,.9);border:none;color:#fff}',
'.slc-confirm-ok:hover{background:rgba(220,38,38,.9);transform:translateY(-1px)}'
].join('');
function injectCSS(id, text) {
if (document.getElementById(id)) return;
const s = h('style', { id, text });
document.head.appendChild(s);
disposers.push(() => s.remove());
}
// ==================== 工具函数 ====================
const escHtml = s => String(s ?? '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/\n/g,' ');
const noDataHtml = () => `${escHtml(t('noData'))}
`;
const fmtD = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
function getTb() {
if (state.cachedTb && !state.cachedTb.isConnected) { state.cachedTb = null; startObs(); }
return state.cachedTb || (state.cachedTb = document.querySelector(SEL.tb));
}
function waitEl(sel, ms = 10000) {
return new Promise((res, rej) => {
const el = document.querySelector(sel);
if (el) return res(el);
let done = false;
const fin = e => { if (done) return; done = true; obs.disconnect(); clearTimeout(t); res(e); };
const root = document.querySelector('#page_body') || document.documentElement;
const obs = new MutationObserver(() => { const e = document.querySelector(sel); if (e) fin(e); });
obs.observe(root, { childList: true, subtree: true });
const t = setTimeout(() => { if (!done) { done = true; obs.disconnect(); rej(new Error(`waitEl timeout: ${sel}`)); } }, ms);
});
}
function classify(text) {
const s = text.trim();
for (const [re, id] of CAT_RE) { if (re.test(s)) return id; }
return 'other';
}
const isHdr = row => {
if (row.querySelector('th')) return true;
const d = row.querySelector('td.license_date_col')?.textContent?.trim() || '';
return d.length > 0 && !/\d/.test(d);
};
function dataRows() {
if (state.cachedDataRows) return state.cachedDataRows;
const tb = getTb();
const rows = tb ? Array.from(tb.rows).filter(r => !isHdr(r)) : [];
state.cachedDataRows = rows;
return rows;
}
const invalidateDataRows = () => { state.cachedDataRows = null; };
const MON = { 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();
const m1 = s.match(/(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日/);
if (m1) return new Date(+m1[1],+m1[2]-1,+m1[3]).getTime();
const m2 = s.match(/([A-Za-z]+)\s+(\d{1,2}),?\s+(\d{4})/);
if (m2) { const mo = MON[m2[1].toLowerCase().slice(0,3)]; if (mo !== undefined) return new Date(+m2[3],mo,+m2[2]).getTime(); }
const m3 = s.match(/(\d{1,2})\s+([A-Za-z]+),?\s+(\d{4})/);
if (m3) { const mo = MON[m3[2].toLowerCase().slice(0,3)]; if (mo !== undefined) return new Date(+m3[3],mo,+m3[1]).getTime(); }
const d = new Date(s); return isNaN(d) ? null : d.getTime();
}
// ==================== DOM Helper ====================
const BOOL_A = new Set(['disabled','checked','selected','readonly','required','hidden','multiple','autofocus','open']);
function h(tag, p = {}, ch = []) {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(p)) {
if (v == null) continue;
if (k === 'style' && typeof v === 'object') { for (const [sk, sv] of Object.entries(v)) { sk.startsWith('--') ? el.style.setProperty(sk, sv) : 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_A.has(k)) { v ? el.setAttribute(k, '') : el.removeAttribute(k); }
else el.setAttribute(k, v);
}
for (const c of ch) el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
return el;
}
// ==================== 昵称 ====================
const getNick = () => document.querySelector('#account_pulldown')?.textContent.trim()
|| document.querySelector('#global_header .user_persona')?.textContent.trim() || '';
function updateTitle() {
const nick = getNick();
if (!nick) return;
const hdr = document.querySelector(SEL.hdr);
if (!hdr) return;
const orig = hdr.dataset.origTitle || hdr.textContent.trim();
const title = t('pageTitle').replace('{nickname}', nick);
let span = hdr.querySelector('.page-title-text');
if (!span) {
span = h('span', { class:'page-title-text', text:title });
hdr.textContent = ''; hdr.appendChild(span);
hdr.style.cssText = 'display:flex;align-items:center;justify-content:space-between;width:100%;cursor:pointer';
span.style.cssText = 'max-width:500px;overflow-wrap:break-word';
}
if (!hdr.dataset.titleInit) {
hdr.dataset.titleInit = '1';
const handler = e => {
if (e.target.closest('.search-box')) return;
const show = hdr.dataset.showOrig === '1';
const newText = show ? hdr.dataset.newTitle : hdr.dataset.origTitle;
// 只更新文本节点,保留子元素(如 .slc-load-status)
const firstNode = span.firstChild;
if (firstNode && firstNode.nodeType === Node.TEXT_NODE) {
firstNode.nodeValue = newText;
} else {
span.insertBefore(document.createTextNode(newText), span.firstChild);
}
hdr.dataset.showOrig = show ? '0' : '1';
};
hdr.addEventListener('click', handler);
disposers.push(() => hdr.removeEventListener('click', handler));
} else { span.textContent = title; }
hdr.dataset.origTitle = orig; hdr.dataset.newTitle = title;
document.title = title;
}
function addBreadcrumb() {
const cur = document.querySelector('.blockbg .breadcrumb_current_page');
if (!cur || cur.dataset.done) return;
cur.dataset.done = '1';
const p = cur.parentElement;
p.appendChild(h('span', { class:'breadcrumb_separator', text:'>' }));
p.appendChild(h('a', { href:'https://store.steampowered.com/account/history', target:'_blank', text:t('historyLink'), 'data-panel':'{"noFocusRing":true}' }));
}
// ==================== UI Refs ====================
const ui = {
btns: new Map(), statTotal: null, statCounts: new Map(), statBars: new Map(), statCards: new Map(),
pageTotal: null, prevBtn: null, nextBtn: null, showAllBtn: null, pageInput: null,
searchInput: null,
donutRefs: null, mpRefs: null, predictRefs: null,
trendRefs: null, similarRefs: null, resizeObs: null,
trendVisible: null, trendCumulative: true, similarPrefixWords: undefined
};
// ==================== Core Logic ====================
function markRows() {
const tb = getTb();
if (!tb) return;
try {
for (const row of tb.rows) {
if (isHdr(row)) { delete row.dataset.category; delete row.dataset.dateTimestamp; delete row.dataset.searchText; continue; }
// 删除 "未找到许可" 的无效行
if (row.textContent.includes('未找到许可')) { row.remove(); continue; }
if (row.dataset.category) continue;
const dc = row.querySelector('td.license_date_col');
const nc = row.querySelector('td:not(.license_date_col):not(.license_acquisition_col):not(.steamdb_license_id_col)');
const ac = row.querySelector('td.license_acquisition_col');
if (!dc || !nc || !ac) continue;
const raw = dc.textContent.trim();
row.dataset.dateTimestamp = parseDate(raw) ?? 0;
const norm = raw.replace(/\s+/g,' ').trim().replace(/ *([年月日]) */g,'$1');
if (dc.textContent !== norm) dc.textContent = norm;
row.dataset.category = classify(ac.textContent);
// 排除 "移除" 按钮文字后提取游戏名(修复 #7:避免 cloneNode 开销)
const removeLink = nc.querySelector('.free_license_remove_link');
const removeText = removeLink?.textContent?.trim() || '';
const fullText = nc.textContent?.trim() || '';
row.dataset.gameName = removeText ? fullText.replace(removeText, '').trim() : fullText;
row.dataset.searchText = row.dataset.gameName.toLowerCase();
}
} catch (err) { console.error('[SLC] markRows error:', err); }
}
function recalc() {
try {
for (const k of state.counts.keys()) state.counts.set(k, 0);
for (const row of dataRows()) {
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);
}
}
} catch (err) { console.error('[SLC] recalc error:', err); }
}
function refreshNums(vis, catCnts) {
const filtering = !!state.search || state.dateFrom || state.dateTo || state.noFree;
const total = filtering ? [...catCnts.values()].reduce((s,v)=>s+v,0) : state.counts.get('total');
ui.statTotal.textContent = total;
for (const c of CATS) {
const val = filtering ? (catCnts.get(c.id)||0) : state.counts.get(c.id);
const el = ui.statCounts.get(c.id);
if (el) el.textContent = val;
const bar = ui.statBars.get(c.id);
if (bar) bar.style.width = total ? (val/total*100)+'%' : '0%';
}
const totalBar = ui.statBars.get('all');
if (totalBar) totalBar.style.width = '100%';
}
function lockWidths() {
const d = String(state.counts.get('total')).length;
if (ui.statTotal) ui.statTotal.style.minWidth = `${d}ch`;
for (const [, el] of ui.statCounts) el.style.minWidth = `${Math.max(1,d-1)}ch`;
}
// 加载期间的轻量统计刷新:仅更新分类和数字,不做分页渲染
function refreshLoadingStats() {
if (!ui.statTotal) return; // 面板尚未创建
invalidateDataRows();
markRows();
recalc();
lockWidths();
// 从 state.counts 构建显示用计数,处理排除免费(搜索/日期筛选在最终applyPage时生效)
const catCnts = new Map(CATS.map(c => [c.id, state.noFree && c.id === 'free' ? 0 : state.counts.get(c.id)]));
refreshNums([], catCnts);
}
function applyFilter(ft) {
const tb = getTb(); if (!tb) return;
state.filter = ft;
if (state.filterCls) tb.classList.remove(state.filterCls);
state.filterCls = ft !== 'all' ? `filter-${ft}` : '';
if (state.filterCls) tb.classList.add(state.filterCls);
ui.btns.forEach((b, id) => b.classList.toggle('active', id === ft));
if (ui.statCards) ui.statCards.forEach((el, id) => { const card = el?.closest('.stat-card'); if (card) card.classList.toggle('active', id === ft); });
state.page = 1; applyPage();
}
function sortRows(rows) {
const [f, dir] = state.sortBy.split('-');
return rows.sort((a, b) => {
let c = 0;
if (f === 'date') c = (+a.dataset.dateTimestamp||0) - (+b.dataset.dateTimestamp||0);
else if (f === 'name') c = (a.dataset.gameName||'').localeCompare(b.dataset.gameName||'');
return dir === 'asc' ? c : -c;
});
}
function getVisible() {
const all = dataRows();
const fromT = state.dateFrom ? new Date(state.dateFrom).getTime() : 0;
const toT = state.dateTo ? new Date(state.dateTo).getTime() + CFG.DAY_MS : Infinity;
const sq = state.search;
const fcat = state.filter;
const vis = [], catCnts = new Map(CATS.map(c=>[c.id,0]));
for (const row of all) {
const cat = row.dataset.category;
if (!cat || cat === 'other') continue;
const rd = +row.dataset.dateTimestamp||0;
if (rd && (rd < fromT || rd > toT)) { row.classList.remove('search-match'); continue; }
const match = !sq || (row.dataset.searchText||'').includes(sq);
row.classList.toggle('search-match', match);
if (!match) continue;
if (state.noFree && cat === 'free') { row.classList.remove('search-match'); continue; }
catCnts.set(cat, (catCnts.get(cat)||0)+1);
if (fcat !== 'all' && cat !== fcat) { row.classList.remove('search-match'); continue; }
vis.push(row);
}
if (state.sortBy !== 'date-desc') sortRows(vis);
return { vis, catCnts };
}
function applyPage() {
try {
const tb = getTb(); if (!tb) return;
const { vis, catCnts } = getVisible();
const hasF = !!state.search || state.dateFrom || state.dateTo || state.noFree;
tb.classList.toggle('search-filtered', hasF);
refreshNums(vis, catCnts);
if (state.showAll) {
tb.classList.remove('paginated'); state.totalPages = 1;
for (const r of tb.rows) r.classList.remove('page-visible');
} else {
tb.classList.add('paginated');
const newTotalPages = Math.max(1, Math.ceil(vis.length / CFG.PAGE_SIZE));
state.totalPages = newTotalPages;
// 修复分页同步:当 totalPages 减少时校正页码
if (state.page > state.totalPages) {
state.page = state.totalPages;
}
state.page = Math.max(1, state.page);
const start = (state.page-1)*CFG.PAGE_SIZE, end = start+CFG.PAGE_SIZE;
const set = new Set(vis.slice(start, end));
for (const r of tb.rows) { if (!r.dataset.category) continue; r.classList.toggle('page-visible', set.has(r)); }
}
updatePager();
} catch (err) { console.error('[SLC] applyPage error:', err); }
}
// ==================== 分页 ====================
function createPager() {
const prev = h('button',{class:'pager-btn','data-action':'prev',text:t('prev')});
const inp = h('input',{type:'text',class:'page-input'});
const tot = h('span',{style:{color:'#c7d5e0',minWidth:'30px',display:'inline-block',textAlign:'center'}});
const sep = h('span',{style:{color:'#8a9ba8'},text:' / '});
const info = h('span',{class:'page-info'},[inp,sep,tot]);
const next = h('button',{class:'pager-btn','data-action':'next',text:t('next')});
const show = h('button',{class:'pager-btn','data-action':'showAll',text:t('showAll')});
inp.addEventListener('blur',()=>handlePageInp(inp));
inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();inp.blur();}});
ui.prevBtn=prev; ui.nextBtn=next; ui.showAllBtn=show; ui.pageTotal=tot; ui.pageInput=inp;
return {prev,info,next,show};
}
function handlePageInp(inp) {
const v = parseInt(inp.value,10);
if (!isNaN(v) && v>=1 && v<=state.totalPages && v!==state.page) { state.page=v; applyPage(); }
else inp.value=state.page;
}
function updatePager() {
if (!ui.prevBtn) return;
const ss = document.querySelector('#slc-sort');
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;
if(ss){ss.disabled=true;ss.value='date-desc';state.sortBy='date-desc';}
} 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; if(ss)ss.disabled=false;
}
ui.pageTotal.textContent=state.totalPages;
}
// ==================== Modal Factory ====================
const ICONS = {
donut: ``,
pie: ``,
predict: ``,
trend: ``,
similar: ``,
key: ``,
info: ``,
calendarHeatmap: ``,
export: ``,
shield: ``,
clock: ``
};
// ==================== 统一分析窗口(顶部标签栏,仿好友管理器家庭组窗口) ====================
const _analysisMeta = {}; // prefix -> { title, iconBg, icon, noteKey }
let _analysisShell = null; // { modal, panelsEl, iconEl, titleEl, noteEl }
function ensureAnalysisShell() {
if (_analysisShell) return _analysisShell;
const modal = document.createElement('div');
modal.id = 'slc-analysis-modal'; modal.className = 'slc-modal';
const tabsHtml = MODAL_NAV.map(nav =>
``
).join('');
modal.innerHTML = ``;
document.body.appendChild(modal);
const hide = () => modal.classList.remove('visible');
modal.addEventListener('click', e => { if (e.target === modal) hide(); });
modal.querySelector('#slc-analysis-close').addEventListener('click', hide);
// 防止滚轮事件穿透到页面
const mc = modal.querySelector('.slc-mc');
mc.addEventListener('wheel', e => { e.stopPropagation(); }, { passive: true });
// 标签点击:始终调用 showFn(内部 hash 未变时仅激活面板;数据变化如切换"排除免费"则自动重渲染)
modal.querySelectorAll('.slc-tab').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
const nav = MODAL_NAV.find(n => n.prefix === btn.dataset.tab);
if (nav) nav.showFn();
});
});
disposers.push(() => { modal.remove(); _analysisShell = null; });
_analysisShell = {
modal,
panelsEl: modal.querySelector('#slc-analysis-panels'),
iconEl: modal.querySelector('#slc-analysis-hi'),
titleEl: modal.querySelector('#slc-analysis-ht'),
noteEl: modal.querySelector('#slc-analysis-note')
};
return _analysisShell;
}
// 激活统一窗口中的指定面板:标题栏图标/标题/渐变背景随标签主题色切换
function activateAnalysisPanel(prefix) {
const shell = ensureAnalysisShell();
const meta = _analysisMeta[prefix] || {};
const nav = MODAL_NAV.find(n => n.prefix === prefix);
const color = nav?.color || meta.iconBg || '#3b82f6';
shell.modal.classList.add('visible');
shell.panelsEl.querySelectorAll('.slc-panel').forEach(p => p.classList.toggle('active', p.dataset.panel === prefix));
ui._lastAnalysisTab = prefix;
shell.modal.querySelectorAll('.slc-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === prefix));
const header = shell.modal.querySelector('#slc-analysis-header');
header.style.background = `linear-gradient(135deg,${color}26,rgba(15,23,42,.8))`;
header.style.borderBottomColor = `${color}40`;
shell.iconEl.style.color = color;
shell.iconEl.innerHTML = meta.icon || ICONS.info;
if (meta.title) shell.titleEl.textContent = meta.title;
shell.noteEl.textContent = t(meta.noteKey || 'dataNote');
}
// 打开统一分析窗口(主面板"许可分析"按钮入口;记住上次标签;showFn 内部 hash 保证数据变化时重渲染)
function openAnalysis() {
const nav = MODAL_NAV.find(n => n.prefix === ui._lastAnalysisTab) || MODAL_NAV[0];
nav.showFn();
}
// 各功能共用统一窗口:bodyEl 为该功能的专属面板容器;modal 为替身对象,
// show* 函数内部的 modal.classList.add('visible') / dataset.hash / querySelector 行为保持不变
function createChartModal(prefix, title, iconBg, noteKey = 'dataNote') {
const shell = ensureAnalysisShell();
const nav = MODAL_NAV.find(n => n.prefix === prefix);
_analysisMeta[prefix] = { title, iconBg, icon: nav?.icon || ICONS[prefix] || ICONS.info, noteKey };
let bodyEl = shell.panelsEl.querySelector(`#slc-${prefix}-body`);
if (!bodyEl) {
const panel = document.createElement('div');
panel.className = 'slc-panel'; panel.dataset.panel = prefix;
bodyEl = document.createElement('div');
bodyEl.id = `slc-${prefix}-body`; bodyEl.className = 'slc-pbody';
panel.appendChild(bodyEl);
shell.panelsEl.appendChild(panel);
}
const proxy = {
dataset: {},
classList: {
add: c => { if (c === 'visible') activateAnalysisPanel(prefix); },
remove: c => { if (c === 'visible') shell.modal.classList.remove('visible'); },
contains: c => c === 'visible' && shell.modal.classList.contains('visible')
},
querySelector: sel => shell.modal.querySelector(sel)
};
return { modal: proxy, hide: () => shell.modal.classList.remove('visible'), bodyEl };
}
const _modalKeys = ['donutRefs','mpRefs','predictRefs','trendRefs','similarRefs','calRefs','delistedRefs'];
// 标签栏配置(tabKey 为标签短文本;年份已并入许可分布 tab)
const MODAL_NAV = [
{ key: 'donutRefs', prefix: 'donut', icon: ICONS.donut, titleKey: 'donutTitle', tabKey: 'tabDonut', showFn: showDonut, color: '#3b82f6' },
{ key: 'mpRefs', prefix: 'mp', icon: ICONS.pie, titleKey: 'mpTitle', tabKey: 'tabMp', showFn: showMp, color: '#06cfbe' },
{ key: 'predictRefs', prefix: 'predict', icon: ICONS.predict, titleKey: 'predictTitle', tabKey: 'tabPredict', showFn: showPredict, color: '#f59e0b' },
{ key: 'trendRefs', prefix: 'trend', icon: ICONS.trend, titleKey: 'trendTitle', tabKey: 'tabTrend', showFn: showTrend, color: '#3b82f6' },
{ key: 'similarRefs', prefix: 'similar', icon: ICONS.similar, titleKey: 'similarTitle', tabKey: 'tabSimilar', showFn: showSimilar, color: '#f97316' },
{ key: 'calRefs', prefix: 'cal', icon: ICONS.calendarHeatmap, titleKey: 'calendarTitle', tabKey: 'tabCal', showFn: showCalendarHeatmap, color: '#10b981' },
{ key: 'delistedRefs', prefix: 'delisted', icon: ICONS.shield, titleKey: 'delistedTitle', tabKey: 'tabDelisted', showFn: showDelisted, color: '#f44336' }
];
function hideAllModals() {
_modalKeys.forEach(k => ui[k]?.modal.classList.remove('visible'));
rs.modal?.classList.remove('visible');
}
function buildGrid(max, pL, pR, pT, cH) {
let grid = '';
for (let i = 0; i <= 5; i++) {
const v = Math.round(max / 5 * i), y = pT + cH - (cH * i / 5);
grid += `${v}`;
}
return grid;
}
function buildStackedSvg({ labels, data, cats, max, svgW, svgH, pL, pR, pT, pB, bW, clickable, compact }) {
const cW = svgW - pL - pR, cH = svgH - pT - pB;
const gap = cW / labels.length;
const grid = buildGrid(max, pL, svgW - pR, pT, cH);
let bars = '';
labels.forEach((label, i) => {
const item = data[i], cx = pL + gap * i + gap / 2;
let sh = 0;
const tot = cats.reduce((s, c) => s + (item.get(c.id) || 0), 0);
let barStack = '';
for (const c of cats) {
const n = item.get(c.id) || 0, ht = max ? (n / max) * cH : 0, y = pT + cH - sh - ht;
barStack += `${label} ${escHtml(c.label)}: ${n} (${tot ? (n / tot * 100).toFixed(1) : 0}%)`;
sh += ht;
}
if (clickable) {
bars += `${barStack}${label}`;
} else {
bars += `${barStack}${label}`;
}
});
// compact 模式:嵌入卡片内(自适应填充、无卡片包装与图例)
if (compact) {
return ``;
}
const legend = cats.map(c => `${escHtml(c.label)}`).join('');
return `${legend}
`;
}
// 构建年度/月度图表数据(年份柱状图与月度下钻共用)
function buildYearChartData(timeUnit, filterYear = null) {
const cats = isNoFree() ? CATS.filter(c => c.id !== 'free') : CATS;
const timeMap = new Map();
for (const row of dataRows()) {
const cat = row.dataset.category;
if (!cat || cat === 'other' || (isNoFree() && cat === 'free')) continue;
const ts = +row.dataset.dateTimestamp || 0;
if (!ts) continue;
const d = new Date(ts);
if (filterYear && d.getFullYear() !== filterYear) continue;
const timeKey = timeUnit === 'year' ? d.getFullYear() : d.getMonth() + 1;
if (!timeMap.has(timeKey)) timeMap.set(timeKey, new Map(cats.map(c => [c.id, 0])));
const td = timeMap.get(timeKey);
if (td.has(cat)) td.set(cat, td.get(cat) + 1);
}
return { cats, timeMap };
}
// ==================== Donut Chart(许可分布:年份柱状图 + 饼图 + 柱状图) ====================
function showDonut() {
if (!ui.donutRefs) ui.donutRefs = createChartModal('donut', t('donutTitle'), '#3b82f6');
const { modal } = ui.donutRefs;
const cnts=chartCounts(), total=cnts.get('total')||0;
const hash=CATS.map(c=>cnts.get(c.id)).join(',');
if(hash===modal.dataset.hash){modal.classList.add('visible');return;}
modal.dataset.hash=hash;
renderDonutTab(cnts, total);
modal.classList.add('visible');
}
// 许可分布默认视图:左年份柱状图(点击下钻)+ 右上饼图 + 右下分布柱状图
function renderDonutTab(cnts, total) {
const { bodyEl } = ui.donutRefs;
// ===== 左:年份堆叠柱状图(占比较大,盛满卡片;点击在卡片内下钻月度分布) =====
const { cats, timeMap } = buildYearChartData('year');
const times=[...timeMap.keys()].sort((a,b)=>a-b);
let yearHtml;
if(!times.length){
yearHtml=noDataHtml();
}else{
const maxVal=Math.max(...times.map(tm=>[...timeMap.get(tm).values()].reduce((s,v)=>s+v,0)));
const svgW=640,svgH=470,pL=44,pR=12,pT=14,pB=32;
const bW=Math.min(38,((svgW-pL-pR)/times.length)*.6);
yearHtml=buildStackedSvg({labels:times,data:times.map(tm=>timeMap.get(tm)),cats,max:maxVal,svgW,svgH,pL,pR,pT,pB,bW,clickable:true,compact:true});
}
// ===== 右上:饼状图(环形图自适应 + 紧凑图例) =====
const dCats=chartCats(), data=dCats.map(c=>({...c,count:cnts.get(c.id)||0})).filter(d=>d.count>0);
const sz=180,sw=30,r=(sz-sw)/2,C=2*Math.PI*r;
let off=0;
const segs=data.map(d=>{
const dash=(d.count/total)*C;
const s=``;
off+=dash; return s;
}).join('');
const pieSvg=``;
const legendHtml=data.map(d=>`${escHtml(d.label)}
${(d.count/total*100).toFixed(1)}%
`).join('');
// ===== 右下:分布柱状图 =====
const maxCount=Math.max(...data.map(d=>d.count),1);
const barCols=data.map(d=>{
const pct=(d.count/maxCount*100).toFixed(1);
return `${d.count}
${escHtml(d.label)}
`;
}).join('');
bodyEl.innerHTML=`
${escHtml(t('tabYear'))}
${yearHtml}
`;
bodyEl.querySelectorAll('.year-bar-group').forEach(g => {
g.addEventListener('click', () => drillToMonth(+g.dataset.year));
});
bodyEl.querySelector('#slc-dt-back').addEventListener('click', () => {
const c = chartCounts();
renderDonutTab(c, c.get('total') || 0);
});
}
// 年份下钻:左卡片内绘制该年 12 个月堆叠柱状图(标题切为月份,右上角显示返回按钮)
function drillToMonth(year) {
const bodyEl = ui.donutRefs?.bodyEl;
const left = bodyEl?.querySelector('#slc-dt-left');
if (!left || !left.isConnected) return;
const { cats: detailCats, timeMap } = buildYearChartData('month', year);
const months = [1,2,3,4,5,6,7,8,9,10,11,12];
const monthData = months.map(m => timeMap.get(m) || new Map(detailCats.map(c => [c.id, 0])));
const sumOf = m => [...m.values()].reduce((s, v) => s + v, 0);
const maxM = Math.max(...monthData.map(sumOf), 1);
const svgW=640,svgH=470,pL=44,pR=12,pT=14,pB=32;
const bW=Math.min(38,((svgW-pL-pR)/12)*.6);
const chartHtml=buildStackedSvg({labels:months.map(m=>m+'月'),data:monthData,cats:detailCats,max:maxM,svgW,svgH,pL,pR,pT,pB,bW,clickable:false,compact:true});
left.querySelector('#slc-dt-left-title').textContent = `${year} · ${t('monthlyDistribution')}`;
left.querySelector('#slc-dt-back').classList.add('show');
left.querySelector('#slc-dt-chart').innerHTML = chartHtml;
}
// ==================== Monthly Share(月度占比标签页) ====================
// 按年→月聚合分类计数(月度占比数据源)
function buildMonthlyMap() {
const cats = isNoFree() ? CATS.filter(c => c.id !== 'free') : CATS;
const map = new Map();
for (const row of dataRows()) {
const cat = row.dataset.category;
if (!cat || cat === 'other' || (isNoFree() && cat === 'free')) continue;
const ts = +row.dataset.dateTimestamp || 0;
if (!ts) continue;
const d = new Date(ts);
const y = d.getFullYear(), m = d.getMonth() + 1;
if (!map.has(y)) map.set(y, new Map());
const ym = map.get(y);
if (!ym.has(m)) ym.set(m, new Map(cats.map(c => [c.id, 0])));
const mm = ym.get(m);
if (mm.has(cat)) mm.set(cat, mm.get(cat) + 1);
}
return { cats, map };
}
// 单月饼图 SVG(学习好友管理器家庭组月度饼图:扇区=分类、中心=总数)
function buildMpPieSvg(entry, cats) {
const slices = cats.map(c => ({ label: c.label, count: entry ? (entry.get(c.id) || 0) : 0, color: c.color })).filter(s => s.count > 0);
const total = slices.reduce((s, sl) => s + sl.count, 0);
if (total === 0) return null;
const r = 38, cx = 50, cy = 50;
let paths = '';
if (slices.length === 1) {
const sl = slices[0];
paths = `${escHtml(sl.label)}: ${sl.count} (100%)`;
} else {
let a0 = -Math.PI / 2;
slices.forEach(sl => {
const ang = (sl.count / total) * Math.PI * 2;
const a1 = a0 + ang;
const x1 = cx + r * Math.cos(a0), y1 = cy + r * Math.sin(a0);
const x2 = cx + r * Math.cos(a1), y2 = cy + r * Math.sin(a1);
const la = ang > Math.PI ? 1 : 0;
const pct = Math.round(sl.count / total * 100);
paths += `${escHtml(sl.label)}: ${sl.count} (${pct}%)`;
a0 = a1;
});
}
paths += `${total}`;
return ``;
}
function showMp() {
if (!ui.mpRefs) ui.mpRefs = createChartModal('mp', t('mpTitle'), '#06cfbe');
const { modal, bodyEl } = ui.mpRefs;
const cnts=chartCounts();
const hash=CATS.map(c=>cnts.get(c.id)).join(',');
if(hash===modal.dataset.hash){modal.classList.add('visible');return;}
modal.dataset.hash=hash;
const { cats: mpCats, map: mpMap } = buildMonthlyMap();
const isEn = locale === 'en';
if(!mpMap.size){
bodyEl.innerHTML=noDataHtml();
modal.classList.add('visible');
return;
}
const legendItems=mpCats.map(c=>`${escHtml(c.label)}`).join('');
const years=[...mpMap.keys()].sort((a,b)=>b-a);
let peak=1,monthCount=0,grand=0;
const groupsHtml=years.map(year=>{
const ym=mpMap.get(year);
let items='',yearTotal=0;
for(let m=1;m<=12;m++){
const entry=ym.get(m);
const mLabel=isEn?String(m).padStart(2,'0'):m+'月';
if(entry){
monthCount++;
const mSum=[...entry.values()].reduce((s,v)=>s+v,0);
yearTotal+=mSum;
[...entry.values()].forEach(v=>{if(v>peak)peak=v;});
items+=`${buildMpPieSvg(entry,mpCats)}
${mLabel}
`;
}else{
items+=``;
}
}
grand+=yearTotal;
return `${year}${t('mpAdded')} ${yearTotal}
${items}
`;
}).join('');
bodyEl.innerHTML=`${legendItems}
${groupsHtml}
${t('total')}: ${grand}${t('mpMonths')}: ${monthCount}${t('mpPeak')}: ${peak} ${t('mpPerMonth')}
`;
modal.classList.add('visible');
}
// ==================== 预测(许可频率标签页) ====================
function buildPredictSum({ rows, udates, apd, avg, lastD, predD }) {
return `
${ICONS.similar}
${escHtml(t('predictTotalItems'))}${rows}
${ICONS.calendarHeatmap}
${escHtml(t('predictTotalGames'))}${udates}
${ICONS.predict}
${escHtml(t('predictAvgPerDay'))}${apd}
${ICONS.clock}
${escHtml(t('predictOverallAvg'))}${avg.toFixed(1)} ${escHtml(t('predictDays'))}
${ICONS.donut}
${escHtml(t('predictLastDate'))}${fmtD(lastD)}
${ICONS.trend}
${escHtml(t('predictNextDate'))}${fmtD(predD)}
`;
}
function buildPredictTable(ym, ycMap, ylMap) {
const yrs = [...ym.keys()].sort((a, b) => b - a);
const tRows = yrs.map(y => {
const info = ym.get(y), a = info.ints.length > 0 ? info.ints.reduce((s, v) => s + v, 0) / info.ints.length : 0;
const cnt = ycMap.get(y) || 0, lic = ylMap.get(y) || 0, daily = cnt > 0 ? (lic / cnt).toFixed(2) : '0.00', pct = (cnt / 365 * 100).toFixed(1);
return `| ${y} | ${lic} | ${cnt} | ${daily} | ${a > 0 ? a.toFixed(1) + ' ' + escHtml(t('predictDays')) : '-'} | |
`;
}).join('');
return `| ${escHtml(t('yearTitle'))} | ${escHtml(t('predictYearLicenses'))} | ${escHtml(t('predictYearCount'))} | ${escHtml(t('predictDailyLicenses'))} | ${escHtml(t('predictYearAvg'))} | ${escHtml(t('predictYearRatio'))} |
${tRows}
`;
}
function showPredict() {
if (!ui.predictRefs) ui.predictRefs = createChartModal('predict', t('predictTitle'), '#f59e0b', 'predictNote');
const { modal, bodyEl } = ui.predictRefs;
const rows = dataRows().filter(r => r.dataset.category && r.dataset.category !== 'free' && +r.dataset.dateTimestamp > 0);
if (rows.length < 2) { bodyEl.innerHTML = noDataHtml(); modal.classList.add('visible'); return; }
const dateSet = new Set(), udates = [];
for (const r of rows) { const ts = +r.dataset.dateTimestamp; if (ts > 0 && !dateSet.has(ts)) { dateSet.add(ts); udates.push(ts); } }
udates.sort((a, b) => a - b);
if (udates.length < 2) { bodyEl.innerHTML = noDataHtml(); modal.classList.add('visible'); return; }
const intervals = [];
for (let i = 1; i < udates.length; i++) intervals.push(Math.round((udates[i] - udates[i - 1]) / CFG.DAY_MS));
const avg = intervals.reduce((s, v) => s + v, 0) / intervals.length;
const ym = new Map();
for (let i = 1; i < udates.length; i++) {
const y = new Date(udates[i]).getFullYear();
if (!ym.has(y)) ym.set(y, { ints: [], cnt: 0 });
ym.get(y).ints.push(Math.round((udates[i] - udates[i - 1]) / CFG.DAY_MS));
ym.get(y).cnt++;
}
const ycMap = new Map(), ylMap = new Map();
for (const ts of udates) { const y = new Date(ts).getFullYear(); ycMap.set(y, (ycMap.get(y) || 0) + 1); }
for (const r of rows) { const ts = +r.dataset.dateTimestamp; if (ts > 0) { const y = new Date(ts).getFullYear(); ylMap.set(y, (ylMap.get(y) || 0) + 1); } }
const lastD = new Date(udates[udates.length - 1]);
const predD = new Date(lastD.getTime() + avg * CFG.DAY_MS);
const apd = udates.length > 0 ? (rows.length / udates.length).toFixed(2) : '0';
bodyEl.innerHTML = `${buildPredictSum({ rows: rows.length, udates: udates.length, apd, avg, lastD, predD })}${buildPredictTable(ym, ycMap, ylMap)}
`;
modal.classList.add('visible');
}
// ==================== 增加曲线(趋势标签页:按月折线,SVG 随窗口自适应盛满) ====================
function showTrend() {
if (!ui.trendRefs) ui.trendRefs = createChartModal('trend', t('trendTitle'), '#3b82f6', 'dataNote');
const { modal, bodyEl } = ui.trendRefs;
// 分类颜色(与主分类统一)
const catColors = { all: '#64748b', ...Object.fromEntries(CATS.map(c => [c.id, c.color])) };
const catOrder = CATS.map(c => c.id);
// 初始化可见性(默认不显示总和,只显示4个分类)
if (!ui.trendVisible) ui.trendVisible = new Set(catOrder);
if (ui.trendCumulative === undefined) ui.trendCumulative = true;
const allRows = dataRows().filter(r => r.dataset.category && r.dataset.category !== 'other' && +r.dataset.dateTimestamp > 0);
// 创建图表容器(筛选按钮行 + 图表卡片盛满面板)
const chartContainerId = 'slc-trend-chart';
bodyEl.innerHTML = `${noDataHtml()}
`;
const chartContainer = bodyEl.querySelector(`#${chartContainerId}`);
// 清理上一次的事件处理器(修复 #2)
if (bodyEl._trendCleanup) { bodyEl._trendCleanup(); bodyEl._trendCleanup = null; }
let trendFilterHandler = null;
let trendCumHandler = null;
function renderTrend() {
const visibleCats = catOrder.filter(c => ui.trendVisible.has(c));
const showAll = ui.trendVisible.has('all');
if (!visibleCats.length && !showAll) { chartContainer.innerHTML = noDataHtml(); return; }
const rows = allRows.filter(r => ui.trendVisible.has(r.dataset.category) || showAll);
if (!rows.length) { chartContainer.innerHTML = noDataHtml(); return; }
// 按月统计每个分类和总和
const emptyMonth = () => ({ all: 0, ...Object.fromEntries(catOrder.map(c => [c, 0])) });
const monthly = new Map();
for (const row of rows) {
const ts = +row.dataset.dateTimestamp;
const d = new Date(ts);
const key = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
if (!monthly.has(key)) monthly.set(key, emptyMonth());
const cat = row.dataset.category;
if (monthly.get(key)[cat] !== undefined) {
monthly.get(key)[cat]++;
monthly.get(key).all++;
}
}
const sorted = [...monthly.entries()].sort((a, b) => a[0].localeCompare(b[0]));
// 累加模式下计算累计值
let data = sorted.map(x => ({ ...x[1] }));
if (ui.trendCumulative) {
const cum = emptyMonth();
data = data.map(d => {
for (const c of ['all', ...catOrder]) cum[c] += d[c];
return { ...cum };
});
}
const max = Math.max(...data.map(x => {
let m = 0;
if (showAll) m = Math.max(m, x.all);
catOrder.forEach(c => { if (ui.trendVisible.has(c)) m = Math.max(m, x[c]); });
return m;
})) || 1;
const svgW = 1000, svgH = 540, pL = 56, pR = 24, pT = 20, pB = 64;
const cW = svgW - pL - pR, cH = svgH - pT - pB;
const step = cW / (sorted.length - 1 || 1);
// 生成每条可见曲线
const lineKeys = [...(showAll ? ['all'] : []), ...visibleCats];
const lines = lineKeys.map(cat => {
const points = data.map((item, i) => {
const x = pL + i * step;
const y = pT + cH - (item[cat] / max * cH);
return `${x},${y}`;
}).join(' ');
return ``;
}).join('');
// 网格
const grid = buildGrid(max, pL, svgW - pR, pT, cH);
// 年份标签
let labels = '';
let lastYear = '';
sorted.forEach((item, i) => {
const x = pL + i * step;
const year = item[0].split('-')[0];
if (year !== lastYear) {
labels += `${year}`;
lastYear = year;
}
});
const svg = ``;
chartContainer.innerHTML = svg;
}
// 筛选按钮
const filterBtns = document.createElement('div');
filterBtns.setAttribute('data-trend-filter-btns', 'true');
filterBtns.style.cssText = 'display:flex;gap:12px;justify-content:center;flex-shrink:0;margin-bottom:10px;padding:12px;background:rgba(0,0,0,.2);border-radius:8px';
// 累加模式切换开关(内联到筛选按钮行)
const cumToggle = document.createElement('div');
cumToggle.style.cssText = 'display:flex;align-items:center;gap:8px;margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.15)';
cumToggle.innerHTML = `${t('trendCumulative')}`;
const cumInput = cumToggle.querySelector('#slc-trend-cum');
const cumTrack = cumToggle.querySelector('.tg-track');
const cumThumb = cumTrack.querySelector('span');
cumInput.checked = ui.trendCumulative;
function updateCumStyle() {
const on = ui.trendCumulative;
cumTrack.style.background = on ? 'rgba(59,130,246,.3)' : '#2a475e';
cumTrack.style.borderColor = on ? '#3b82f6' : '#4a6a7a';
cumThumb.style.left = on ? '16px' : '2px';
cumThumb.style.background = on ? '#3b82f6' : '#8a9ba8';
}
updateCumStyle();
trendCumHandler = () => {
ui.trendCumulative = cumInput.checked;
updateCumStyle();
renderTrend();
};
cumInput.addEventListener('change', trendCumHandler);
// 注册清理函数,下次打开时移除旧监听器(修复 #2)
bodyEl._trendCleanup = () => {
cumInput.removeEventListener('change', trendCumHandler);
filterBtns.removeEventListener('click', trendFilterHandler);
};
function updateFilterBtns() {
const allVisible = ui.trendVisible.has('all');
filterBtns.innerHTML = `` + catOrder.map(cat => {
const visible = ui.trendVisible.has(cat);
return ``;
}).join('');
filterBtns.appendChild(cumToggle);
}
updateFilterBtns();
bodyEl.insertBefore(filterBtns, chartContainer);
renderTrend();
// 绑定筛选事件(使用引用以便清理)
trendFilterHandler = e => {
const btn = e.target.closest('button');
if (!btn) return;
const cat = btn.dataset.cat;
if (ui.trendVisible.has(cat)) {
ui.trendVisible.delete(cat);
} else {
ui.trendVisible.add(cat);
}
renderTrend();
updateFilterBtns();
};
filterBtns.addEventListener('click', trendFilterHandler);
modal.classList.add('visible');
}
// ==================== Similar License Guess ====================
function showSimilar() {
if (!ui.similarRefs) ui.similarRefs = createChartModal('similar', t('similarTitle'), '#f97316', 'dataNote');
const { modal, bodyEl } = ui.similarRefs;
// 获取所有非免费游戏名称
const catMap = new Map(CATS.map(c => [c.id, c]));
const rows = dataRows().filter(r => r.dataset.gameName && r.dataset.category && r.dataset.category !== 'other' && !(isNoFree() && r.dataset.category === 'free'));
const names = rows.map(r => {
const cat = catMap.get(r.dataset.category);
return {
name: r.dataset.gameName.trim(),
cat: r.dataset.category,
catLabel: cat?.label || r.dataset.category,
catColor: cat?.color || '#64748b'
};
});
if (names.length < 2) {
bodyEl.innerHTML = noDataHtml();
modal.classList.add('visible');
return;
}
// 前缀单词数选择
if (ui.similarPrefixWords === undefined) ui.similarPrefixWords = 5;
let prefixWords = ui.similarPrefixWords;
const prefixBtns = Array.from({length:10},(_,i)=>i+1).map(n=>``).join('');
const prefixBar = `${t('similarPrefixLen')}:${prefixBtns}
`;
function getWordPrefix(name, count) {
// 按空格/特殊字符分词,取前 count 个单词
const words = name.split(/[\s\-:_,]+/).filter(w => w.length > 0);
return { key: words.slice(0, count).join(' ').toLowerCase(), display: words.slice(0, count).join(' ') };
}
function groupByPrefix(wordCount) {
const groups = new Map();
for (const item of names) {
const { key, display } = getWordPrefix(item.name, wordCount);
const words = item.name.split(/[\s\-:_,]+/).filter(w => w.length > 0);
if (words.length < wordCount) continue;
if (!groups.has(key)) groups.set(key, { display, items: [] });
groups.get(key).items.push(item);
}
return [...groups.entries()].filter(g => g[1].items.length >= 2).sort((a, b) => b[1].items.length - a[1].items.length);
}
function renderSimilar(count) {
const groups = groupByPrefix(count);
if (!groups.length) {
return noDataHtml();
}
const list = groups.map((g, i) => {
const [key, { display, items }] = g;
const itemList = items.map(it => `${escHtml(it.name)}${escHtml(it.catLabel)}
`).join('');
return `${escHtml(display)}${t('similarCount')}: ${items.length} ${itemList}
`;
}).join('');
return `${list}
`;
}
bodyEl.innerHTML = prefixBar + `${renderSimilar(prefixWords)}
`;
// 折叠/展开事件:bodyEl 在多次打开间复用,只绑定一次
if (!bodyEl._similarFoldBound) {
bodyEl._similarFoldBound = true;
bodyEl.addEventListener('click', e => {
const hd = e.target.closest('.slc-similar-hd');
if (!hd) return;
const card = hd.closest('.slc-similar-group');
const bd = card.querySelector('.slc-similar-bd');
const arrow = hd.querySelector('.slc-similar-arrow');
const expanded = bd.style.maxHeight !== '0px';
if (expanded) {
bd.style.maxHeight = '0px';
bd.style.padding = '0 16px';
arrow.style.transform = 'rotate(-90deg)';
} else {
bd.style.maxHeight = bd.scrollHeight + 32 + 'px';
bd.style.padding = '12px 16px';
arrow.style.transform = 'rotate(0deg)';
}
});
}
// 前缀单词数选择事件:old firstElementChild 随 innerHTML 替换被销毁,无需手动解绑
bodyEl.firstElementChild.addEventListener('click', e => {
const btn = e.target.closest('.slc-similar-pbtn');
if (!btn) return;
prefixWords = ui.similarPrefixWords = +btn.dataset.n;
bodyEl.querySelectorAll('.slc-similar-pbtn').forEach(b => {
const active = +b.dataset.n === prefixWords;
b.style.background = active ? 'rgba(249,115,22,.2)' : 'transparent';
b.style.borderColor = active ? '#f97316' : 'rgba(255,255,255,.2)';
b.style.color = active ? '#f97316' : '#8a9ba8';
});
bodyEl.querySelector('#slc-similar-result').innerHTML = renderSimilar(prefixWords);
});
modal.classList.add('visible');
}
// ==================== CDKey Redeem ====================
const FAIL_MAP = {14:'invalidKey',15:'duplicateKey',53:'rateLimit',13:'regionRestrict',9:'alreadyOwned',24:'missingDep',50:'walletCode'};
const SVG_OK='';
const SVG_FAIL='';
const rs = { keyN:0, recvN:0, failSet:new Set(), modal:null, batchT:null, rowMap:new Map(), refs:{}, activeRequests: 0 };
const extractKeys = text => [...new Set((text||'').trim().toUpperCase().match(/([0-9A-Z]{5}-){2,4}[0-9A-Z]{5}/g)||[])];
function showRedeem(){if(!rs.modal)createRedeem();rs.modal.classList.add('visible');}
function redeemDone(){
const{refs:{act:$$a,inp:$$i,stTotal:$$t,stSuccess:$$s,stFail:$$f}}=rs;
const fc=rs.failSet.size,sc=rs.recvN-fc;
if($$t)$$t.textContent=rs.recvN;
if($$s)$$s.textContent=sc;
if($$f)$$f.textContent=fc;
if(rs.recvN>=rs.keyN){
if($$a)$$a.disabled=false;if($$i)$$i.disabled=false;
toast(t('redeemDone').replace('{total}',rs.keyN).replace('{success}',sc).replace('{fail}',fc),fc>0?'error':'success',5000);
}
}
const REDEEM_SVG = {
key: '',
total: '',
ok: '',
fail: ''
};
function createRedeem(){
const modal=document.createElement('div');
modal.id='slc-redeem-modal';modal.className='slc-modal';
modal.innerHTML=`
| ${t('indexLabel')} | Key | ${t('resultLabel')} | ${t('detailLabel')} | Sub | ${t('itemLabel')} |
|---|
${REDEEM_SVG.total}
${t('totalLabel')}0
${REDEEM_SVG.ok}
${t('successLabel')}0
${REDEEM_SVG.fail}
${t('failLabel')}0
${ICONS.info}${t('dataNote')}
`;
document.body.appendChild(modal);
const $=s=>modal.querySelector(s);
const $inp=$('#slc-rinp'),$tbody=$('#slc-rtb'),$table=$('#slc-rt'),$act=$('#slc-ra');
const $stTotal=$('#slc-rs-total'),$stSuccess=$('#slc-rs-success'),$stFail=$('#slc-rs-fail');
rs.refs={inp:$inp,tbody:$tbody,table:$table,act:$act,stTotal:$stTotal,stSuccess:$stSuccess,stFail:$stFail};
const hideModal=()=>rs.modal.classList.remove('visible');
modal.addEventListener('click',e=>{if(e.target===modal)hideModal();});
$('#slc-redeem-close').addEventListener('click',hideModal);
$('#slc-rpaste').addEventListener('click',e=>{stopEv(e,()=>navigator.clipboard.readText().then(text=>{const keys=extractKeys(text);$inp.value=keys.reduce((s,k,i)=>s+(i%4===0&&i?'\n':' ')+k,'').trim();toast(keys.length?t('pasteKeyCount').replace('{count}',keys.length):t('pasteNoKey'),keys.length?'success':'error');}).catch(()=>{}));});
$('#slc-rclear').addEventListener('click',e=>{stopEv(e,()=>{$inp.value='';toast(t('inputCleared'),'info');});});
$('#slc-rch').addEventListener('click',e=>{stopEv(e,()=>{$tbody.innerHTML='';$table.style.display='none';$act.disabled=false;$inp.disabled=false;rs.failSet.clear();rs.rowMap.clear();const stats=$('#slc-rstats');if(stats)stats.style.display='none';['slc-rch','slc-rcf','slc-rcs'].forEach(id=>{const b=$('#'+id);if(b)b.style.display='none';});toast(t('clearBtn'),'info');});});
$('#slc-rcf').addEventListener('click',e=>{stopEv(e,()=>{if(rs.failSet.size){GM_setClipboard([...rs.failSet].join(','));toast(`${t('copiedKeys')} (${rs.failSet.size})`,'success');}else{toast(t('noFailedKeys'),'error');}});});
$('#slc-rcs').addEventListener('click',e=>{stopEv(e,()=>{const lines=[...modal.querySelectorAll('#slc-rtb .rs')].map(row=>{const subId=row.querySelector('.rsub code')?.textContent.trim()||'';const gn=row.querySelector('.rgame')?.textContent.trim()||'';return(subId&&subId!=='——')?`${subId}\t${gn}`:'';}).filter(Boolean);if(lines.length){GM_setClipboard(lines.join('\n'));toast(`${t('copiedKeys')} (${lines.length})`,'success');}else{toast(t('noSubData'),'error');}});});
$('#slc-ra').addEventListener('click',e=>{stopEv(e,()=>{const keys=extractKeys($inp.value);if(keys.length)doRedeem(keys);else toast(t('noKeyInput'),'error');});});
rs.modal=modal;
disposers.push(()=>{clearInterval(rs.batchT);modal?.remove();rs.modal=null;});
}
function doRedeem(keys){
const{inp,tbody,table,act}=rs.refs;
const $=s=>rs.modal.querySelector(s);
table.style.display='';act.disabled=true;inp.disabled=true;
['slc-rch','slc-rcf','slc-rcs'].forEach(id=>{const b=$('#'+id);if(b)b.style.display='';});
const stats=$('#slc-rstats');if(stats)stats.style.display='';
rs.keyN=keys.length;rs.recvN=0;rs.failSet.clear();rs.rowMap.clear();rs.activeRequests=0;
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);rs.rowMap.set(key,row);
if(iCFG.REDEEM_MAX)startBatch();
}
function redeemOne(key){
const fail=(detail,rk='networkError')=>updateRow(key,'fail',detail,rk,0,'');
const sessionID = getSessionID();
if (!sessionID) { fail('No session ID', 'othersError'); return; }
rs.activeRequests++;
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=${encodeURIComponent(key)}&sessionid=${encodeURIComponent(sessionID)}`,method:'POST',responseType:'json',timeout:15000,
onload(resp){
rs.activeRequests--;
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)updateRow(key,'success',t('successDetail'),'',Number(it?.packageid)||0,it?.line_item_description||'');
else{const rk=FAIL_MAP[d.purchase_result_details]||'othersError';updateRow(key,'fail',t(rk),rk,Number(it?.packageid)||0,it?.line_item_description||'');}
},ontimeout:()=>{rs.activeRequests--;fail(t('networkError'));},
onerror:()=>{rs.activeRequests--;fail(t('networkError'));}
});
}
function updateRow(key,status,detail,rk,subId,subName){
const ok=status==='success',isFail=status==='fail';
if(!ok)rs.failSet.add(key);
rs.recvN++;
const row=rs.rowMap.get(key);if(!row)return;
row.className=ok?'rs':(isFail?'rf':'');
const cells=row.querySelectorAll('td');
cells[2].innerHTML=ok?SVG_OK+t('success'):(isFail?SVG_FAIL+t('fail'):detail);
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';
redeemDone();
}
function startBatch(){
clearInterval(rs.batchT);
rs.batchT=setInterval(()=>{
const waiting=rs.modal.querySelectorAll('#slc-rtb tr [data-waiting="true"]');
if(!waiting.length){
// 检查是否还有活跃请求
if (rs.activeRequests === 0) {
clearInterval(rs.batchT);
rs.batchT = null;
}
return;
}
for(const cell of waiting){
if(rs.activeRequests>=CFG.REDEEM_MAX)return;
cell.textContent=`${t('redeeming')}...`;
cell.removeAttribute('data-waiting');
redeemOne(cell.closest('tr').dataset.key);
}
},CFG.REDEEM_WAIT*1000);
}
// ==================== 日历热力图 ====================
const CAL_MONTHS = { zh:['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'], en:['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] };
const CAL_WEEKS = { zh:['日','一','二','三','四','五','六'], en:['Su','Mo','Tu','We','Th','Fr','Sa'] };
function showCalendarHeatmap(){
const rows=dataRows().filter(r=>{
const cat=r.dataset.category;
if(!cat||cat==='other')return false;
if(state.noFree&&cat==='free')return false;
return +r.dataset.dateTimestamp>0;
});
if(!rows.length){toast(t('calendarNoData'),'error');return;}
const dateMap=new Map();
for(const r of rows){
const ts=+r.dataset.dateTimestamp;
const d=new Date(ts);
const key=fmtD(d);
dateMap.set(key,(dateMap.get(key)||0)+1);
}
const dates=[...dateMap.entries()].sort((a,b)=>a[0].localeCompare(b[0]));
const years=[...new Set(dates.map(([d])=>d.slice(0,4)))].sort((a,b)=>b.localeCompare(a));
const maxCount=Math.max(...dateMap.values());
const getColor=v=>{
if(v===0)return'#2a475e';
const p=v/maxCount;
if(p<0.25)return'#0e4429';
if(p<0.5)return'#006d32';
if(p<0.75)return'#26a641';
return'#39d353';
};
const isZh = locale.startsWith('zh');
const monthLabels = isZh ? CAL_MONTHS.zh : CAL_MONTHS.en;
const weekDayLabels = isZh ? CAL_WEEKS.zh : CAL_WEEKS.en;
const buildYear=y=>{
const startDate=new Date(`${y}-01-01`);
const endDate=new Date(`${y}-12-31`);
const startDay=startDate.getDay();
const totalDays=Math.round((endDate-startDate)/CFG.DAY_MS)+1;
let html=`${y}
`;
html+=`
${monthLabels.map(m=>`${m}`).join('')}
`;
html+=`
`;
html+=`
${weekDayLabels.map(d=>`${d}`).join('')}
`;
html+=`
`;
for(let i=0;i
`;
for(let i=0;i
`;
}
html+=`
`;
return html;
};
if (!ui.calRefs) ui.calRefs = createChartModal('cal', t('calendarTitle'), '#10b981');
const { modal, bodyEl } = ui.calRefs;
// hash 缓存:数据未变时跳过重建
const hash = rows.length + '_' + (rows[0]?.dataset.dateTimestamp || '');
if (hash === modal.dataset.hash) { modal.classList.add('visible'); return; }
modal.dataset.hash = hash;
bodyEl.innerHTML = `${years.map(buildYear).join('')}
`;
// 图例放在 bodyEl 外面,不跟随滚动
const legendCls = 'slc-cal-legend';
let legend = bodyEl.nextElementSibling;
if (!legend || !legend.classList.contains(legendCls)) {
bodyEl.insertAdjacentHTML('afterend', `${t('calendarLess')}${t('calendarMore')} `);
}
modal.classList.add('visible');
}
// ==================== 导出 ====================
const _RE_REMOVE = /\s*(移除|Remove|删除|Delete|卸载|Uninstall)\s*/gi;
const _CAT_LABEL_MAP = new Map(CATS.map(c => [c.id, c.label]));
function getExportData(){
const re=_RE_REMOVE;
return dataRows().filter(r=>r.dataset.category&&r.dataset.category!=='other'&&!(state.noFree&&r.dataset.category==='free')).map(r=>({
date:r.querySelector('td.license_date_col')?.textContent.trim()||'',
name:(r.dataset.gameName||'').replace(re,'').trim(),
category:_CAT_LABEL_MAP.get(r.dataset.category)||r.dataset.category,
details:r.querySelector('td.license_acquisition_col')?.textContent.trim()||''
}));
}
function dlBlob(blob,name){
const url=URL.createObjectURL(blob),a=h('a',{href:url,download:name,style:{display:'none'}});
document.body.appendChild(a);a.click();
setTimeout(()=>{a.remove();URL.revokeObjectURL(url);},CFG.BLOB_CLEANUP);
}
function exportData(fmt){
try{
const data=getExportData();
if(!data.length){toast(t('exportNoData'),'error');return;}
const now=new Date(),ts=fmtD(now)+'_'+[now.getHours(),now.getMinutes(),now.getSeconds()].map(v=>String(v).padStart(2,'0')).join('');
if(fmt==='csv'){
const hdr=[t('csvDate'),t('csvName'),t('csvCategory'),t('csvDetails')];
const esc=s=>`"${String(s).replace(/"/g,'""')}"`;
const csv=[hdr.join(','),...data.map(r=>[r.date,r.name,r.category,r.details].map(esc).join(','))].join('\n');
dlBlob(new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'}),`steam-licenses-${ts}.csv`);
}else{
dlBlob(new Blob([JSON.stringify(data,null,2)],{type:'application/json;charset=utf-8;'}),`steam-licenses-${ts}.json`);
}
toast(t('exportSuccess'),'success');
}catch(err){console.error(`Export ${fmt} failed:`,err);toast(t('exportFailed')+': '+err.message,'error');}
}
// ==================== Main UI ====================
function addFilterBtns(){
const table=document.querySelector(SEL.tbl);
if(!table||document.querySelector(SEL.fc))return;
const ctn=h('div',{id:'steam-license-filter'});
// ===== 统计卡片区 =====
const statsRow=h('div',{class:'stats-row'});
const allCats=[{id:'all',color:ALL_TYPE.color,label:ALL_TYPE.label,icon:ALL_TYPE.icon},...CATS.map(c=>({id:c.id,color:c.color,label:c.label,icon:c.icon}))];
const statCards=new Map();
const statBars=new Map();
for(const c of allCats){
const card=h('div',{class:'stat-card'+(c.id==='all'?' active':''),'data-filter':c.id,style:{'--card-color':c.color}});
const num=h('div',{class:'stat-number',text:'0'});
const lbl=h('div',{class:'stat-label',html:c.icon+' '+c.label});
const bar=h('div',{class:'stat-bar'});
const barFill=h('div',{class:'stat-bar-fill',style:{width:'0%',background:c.color}});
bar.appendChild(barFill);
card.append(lbl,num,bar);
statsRow.appendChild(card);
statCards.set(c.id,num);
statBars.set(c.id,barFill);
if(c.id==='all') ui.statTotal=num;
else ui.statCounts.set(c.id,num);
// 免费卡片右侧:排除免费开关 + 功能按钮
if(c.id==='free'){
// 排除免费开关(独立容器)
const nfWrap=h('div',{class:'stat-tools',style:{gap:'6px',padding:'0 12px'}});
const nfLabel=h('label',{id:'slc-no-free',html:`${t('withoutFree')}`});
nfWrap.appendChild(nfLabel);
statsRow.appendChild(nfWrap);
// 功能按钮 - 单排布局:许可分析(合并窗口)、激活、导出、清除缓存
const tools=h('div',{class:'stat-tools',style:{gap:'4px'}});
const svg16 = s => s.replace('