// ==UserScript== // @name Steam Social Center // @namespace https://github.com/SmallRob/steam-userscripts // @version 1.0.0 // @description 社交中心界面。对照宿主 steam-activity-modern 4.9.0(基线 4.8.1)。样式前缀 .ssc-,不改主脚本 .sf- 样式。 // @author SmallFork & SmallRob // @license MIT // ==/UserScript== /** * window.SteamSocialCenter.init(host) → boolean * 三个页:好友数据、好友排行、游戏比对。 * 打开时只读 steam_social_db,不自动跑等级和游戏库索引。 */ (function (root) { 'use strict'; const VERSION = '1.0.0'; const API_VERSION = 1; const ROW = 72; const CSS = ` #ssc-root{position:fixed;inset:0;z-index:100000;display:flex;flex-direction:column;background:#171a21;color:#c7d5e0;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;opacity:0;visibility:hidden;transition:opacity .2s ease} #ssc-root.ssc-open{opacity:1;visibility:visible} .ssc-top{display:flex;align-items:center;gap:16px;padding:14px 20px;background:rgba(23,26,33,.96);border-bottom:1px solid rgba(255,255,255,.08);flex-shrink:0} .ssc-title{font-size:18px;font-weight:700;color:#fff;margin-right:8px} .ssc-tabs{display:flex;gap:6px;flex:1} .ssc-tab{border:1px solid transparent;background:rgba(255,255,255,.04);color:#c7d5e0;border-radius:8px;padding:7px 12px;font-size:13px;font-weight:600;cursor:pointer} .ssc-tab.ssc-on{background:linear-gradient(135deg,#0ea5e9,#0284c7);color:#fff} .ssc-x{width:34px;height:34px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.06);color:#fff;cursor:pointer;font-size:16px} .ssc-x:hover{background:rgba(244,63,94,.25)} .ssc-body{flex:1;min-height:0;display:flex;flex-direction:column;padding:14px 20px 18px} .ssc-page{flex:1;min-height:0;display:none;flex-direction:column;gap:10px} .ssc-page.ssc-on{display:flex} .ssc-kpis{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px} .ssc-kpi{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:8px 10px} .ssc-kpi b{display:block;font-size:18px;color:#fff;font-weight:700} .ssc-kpi span{font-size:11px;color:#8f98a0} .ssc-tools{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .ssc-tools input,.ssc-tools select,.ssc-field input,.ssc-field select{background:#1b2838;color:#fff;border:1px solid rgba(102,192,244,.25);border-radius:8px;padding:7px 10px;font-size:13px} .ssc-tools input{min-width:220px;flex:1} .ssc-btn{border:none;border-radius:8px;padding:7px 12px;font-size:12px;font-weight:700;cursor:pointer;color:#fff;background:#1b2838;border:1px solid rgba(255,255,255,.08)} .ssc-btn.ssc-primary{background:linear-gradient(135deg,#0ea5e9,#0369a1)} .ssc-btn:disabled{opacity:.45;cursor:default} .ssc-note{font-size:12px;color:#94a3b8} .ssc-banner{background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.35);color:#fbbf24;border-radius:8px;padding:8px 10px;font-size:12px} .ssc-list{position:relative;flex:1;min-height:0;overflow:auto;border:1px solid rgba(255,255,255,.06);border-radius:12px;background:rgba(0,0,0,.15)} .ssc-spacer{width:1px} .ssc-win{position:absolute;left:0;right:0;top:0} .ssc-row{display:flex;align-items:center;gap:10px;height:64px;margin:0 8px 8px;padding:0 10px;box-sizing:border-box;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.05);border-radius:10px;color:inherit;width:calc(100% - 16px);text-align:left;cursor:pointer} .ssc-row:hover{border-color:rgba(14,165,233,.45);background:rgba(14,165,233,.08)} .ssc-av{width:40px;height:40px;border-radius:50%;object-fit:cover;background:#2a475e;flex-shrink:0} .ssc-mid{flex:1;min-width:0} .ssc-name{font-size:14px;font-weight:700;color:#f8fafc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .ssc-sub{font-size:12px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .ssc-meta{display:flex;gap:6px;flex-shrink:0;font-size:11px;color:#cbd5e1} .ssc-pill{background:rgba(255,255,255,.06);border-radius:999px;padding:3px 7px} .ssc-pill.ssc-new{background:rgba(244,63,94,.18);color:#fda4af} .ssc-pill.ssc-vac{background:rgba(244,63,94,.2);color:#fecaca} .ssc-pill.ssc-game{background:rgba(139,92,246,.2);color:#ddd6fe} .ssc-empty{margin:auto;text-align:center;color:#8f98a0;padding:48px 16px;font-size:14px;line-height:1.6} .ssc-rank-list,.ssc-cmp{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:6px} .ssc-rank-row,.ssc-game{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.04)} .ssc-rank-row b{width:36px;color:#7dd3fc;flex-shrink:0} .ssc-rank-row button,.ssc-game button{margin-left:auto} .ssc-split{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;min-height:0;flex:1} .ssc-card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:12px;display:flex;flex-direction:column;gap:8px;min-height:0;min-width:0} .ssc-card h3{margin:0;font-size:14px;color:#fff} .ssc-checks{overflow:auto;max-height:180px;display:flex;flex-direction:column;gap:4px;font-size:12px} .ssc-result{overflow:auto;flex:1;min-height:80px;font-size:12px;display:flex;flex-direction:column;gap:4px} .ssc-suggest{display:flex;flex-direction:column;gap:4px} .ssc-suggest button{text-align:left} @media(max-width:900px){.ssc-kpis{grid-template-columns:repeat(3,minmax(0,1fr))}.ssc-split{grid-template-columns:1fr}.ssc-body{padding:10px}} `; let host = null; let core = null; let inited = false; let styled = false; let opened = false; let page = 'data'; let query = ''; let sortKey = 'days'; let view = []; let rootEl = null; let searchTimer = null; let gameTimer = null; let comparePick = []; function $(sel, base) { return (base || rootEl).querySelector(sel); } function text(sel, value) { const node = $(sel); if (node) node.textContent = value; } function init(nextHost) { try { if (!nextHost || nextHost.apiVersion !== API_VERSION) return false; const required = ['getApiKey', 'getMySteamId', 'openFriend', 'showToast']; for (let i = 0; i < required.length; i++) { if (typeof nextHost[required[i]] !== 'function') return false; } core = root.SteamSocialCore; if (!core || core.apiVersion !== API_VERSION || typeof core.attach !== 'function') return false; if (!core.attach(nextHost)) return false; host = nextHost; injectStyle(); inited = true; return true; } catch (e) { return false; } } function injectStyle() { if (styled) return; styled = true; if (typeof GM_addStyle === 'function') GM_addStyle(CSS); else if (root.document) { const style = root.document.createElement('style'); style.textContent = CSS; root.document.head.appendChild(style); } } function isOpen() { return opened; } function toast(message, type) { try { host.showToast(message, type || 'info'); } catch (e) { /* 宿主提示失败时忽略 */ } } function ensureDom() { if (rootEl) return; const doc = root.document; rootEl = doc.createElement('div'); rootEl.id = 'ssc-root'; rootEl.innerHTML = `
社交中心
已索引 0 / 0
游戏数、总时长、近两周只统计已经索引的好友。索引不会在打开时自动开始。

一对一

2–4 人交集

单游戏时长

`; doc.body.appendChild(rootEl); rootEl.querySelector('.ssc-x').addEventListener('click', close); rootEl.querySelectorAll('.ssc-top .ssc-tab').forEach((btn) => { btn.addEventListener('click', () => showPage(btn.dataset.page)); }); $('#ssc-q').addEventListener('input', (e) => { query = e.target.value || ''; clearTimeout(searchTimer); searchTimer = setTimeout(() => paintList(false), 120); }); $('#ssc-sort').addEventListener('change', (e) => { sortKey = e.target.value; paintList(false); }); $('#ssc-refresh').addEventListener('click', () => refreshRoster(true)); $('#ssc-level').addEventListener('click', onLevelClick); $('#ssc-list').addEventListener('scroll', onListScroll); $('#ssc-list').addEventListener('click', onRowClick); rootEl.querySelectorAll('[data-rank]').forEach((btn) => { btn.addEventListener('click', () => { rootEl.querySelectorAll('[data-rank]').forEach((node) => node.classList.toggle('ssc-on', node === btn)); renderRank(btn.dataset.rank); }); }); $('#ssc-index').addEventListener('click', onIndexClick); $('#ssc-pair-go').addEventListener('click', renderPair); $('#ssc-many-go').addEventListener('click', renderMany); $('#ssc-game-q').addEventListener('input', () => { clearTimeout(gameTimer); gameTimer = setTimeout(renderGameSuggest, 200); }); buildKpiShell(); } function buildKpiShell() { const keys = [ ['total', '好友'], ['ingame', '游戏中'], ['online', '在线'], ['offline', '离线'], ['fresh', '30 天内新好友'], ['countries', '国家'], ['vac', 'VAC 已知'], ]; const hostNode = $('#ssc-kpis'); hostNode.innerHTML = ''; keys.forEach((item) => { const card = document.createElement('div'); card.className = 'ssc-kpi'; const value = document.createElement('b'); value.id = 'ssc-kpi-' + item[0]; value.textContent = '–'; const label = document.createElement('span'); label.textContent = item[1]; card.appendChild(value); card.appendChild(label); hostNode.appendChild(card); }); } function showPage(next) { page = next || 'data'; rootEl.querySelectorAll('.ssc-top .ssc-tab').forEach((btn) => { btn.classList.toggle('ssc-on', btn.dataset.page === page); }); rootEl.querySelectorAll('.ssc-page').forEach((node) => { node.classList.toggle('ssc-on', node.dataset.page === page); }); if (page === 'data') paintList(true); if (page === 'rank') { syncIndexButton(); const current = rootEl.querySelector('[data-rank].ssc-on'); renderRank(current ? current.dataset.rank : 'days'); } if (page === 'compare') fillCompareSources(); } async function open() { if (!inited) return; ensureDom(); opened = true; rootEl.classList.add('ssc-open'); document.body.style.overflow = 'hidden'; document.addEventListener('keydown', onKey); showPage(page); try { await core.ready; } catch (e) { /* 内存模式仍可打开 */ } paintFromCache(); const key = safeKey(); const banner = $('#ssc-key-banner'); if (banner) banner.hidden = !!key; if (!key) return; if (!core.rosterFresh()) refreshRoster(false); else core.startVacFill(onVac); } function close() { if (!opened && !rootEl) return; opened = false; if (core) { try { core.pauseLevelJob(); } catch (e) { /* 无任务 */ } try { core.pauseLibraryIndexJob(); } catch (e) { /* 无任务 */ } try { core.pauseVacFill(); } catch (e) { /* 无任务 */ } } document.removeEventListener('keydown', onKey); if (rootEl) rootEl.classList.remove('ssc-open'); document.body.style.overflow = ''; } function onKey(e) { if (e.key === 'Escape' && opened) close(); } function safeKey() { try { return host.getApiKey(); } catch (e) { return ''; } } function paintFromCache() { paintKpi(); paintList(true); syncLevelButton(); syncIndexButton(); } function paintKpi() { const data = core.kpi(); text('#ssc-kpi-total', String(data.total)); text('#ssc-kpi-ingame', String(data.ingame)); text('#ssc-kpi-online', String(data.online)); text('#ssc-kpi-offline', String(data.offline)); text('#ssc-kpi-fresh', String(data.fresh)); text('#ssc-kpi-countries', String(data.countries)); text('#ssc-kpi-vac', data.vacKnown + (data.vacBanned ? ' · 封禁 ' + data.vacBanned : '')); } function paintList(keepScroll) { if (!rootEl) return; view = core.queryFriends({ q: query, sort: sortKey }); const list = $('#ssc-list'); const top = keepScroll ? list.scrollTop : 0; const spacer = list.querySelector('.ssc-spacer'); spacer.style.height = (view.length * ROW) + 'px'; if (!view.length) { list.querySelector('.ssc-win').textContent = ''; const empty = document.createElement('div'); empty.className = 'ssc-empty'; empty.textContent = core.loadRoster() ? '没有匹配的好友' : (safeKey() ? '还没有好友缓存,正在准备名单…' : '没有缓存。填写 API Key 后才能拉取好友名单。'); list.querySelector('.ssc-win').appendChild(empty); return; } if (!keepScroll) list.scrollTop = 0; else list.scrollTop = top; drawWindow(); requestAnimationFrame(() => { if (opened) drawWindow(); }); } let scrollRaf = 0; function onListScroll() { if (scrollRaf) return; scrollRaf = requestAnimationFrame(() => { scrollRaf = 0; drawWindow(); }); } function drawWindow() { const list = $('#ssc-list'); if (!list || !view.length) return; const top = list.scrollTop; const height = list.clientHeight || 480; const start = Math.max(0, Math.floor(top / ROW) - 4); const end = Math.min(view.length, Math.ceil((top + height) / ROW) + 4); const win = list.querySelector('.ssc-win'); const vac = core.getVac(); const levels = core.getLevels(); const frag = document.createDocumentFragment(); for (let i = start; i < end; i++) frag.appendChild(renderRow(view[i], vac, levels)); win.textContent = ''; win.style.transform = 'translateY(' + (start * ROW) + 'px)'; win.appendChild(frag); } function renderRow(friend, vac, levels) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ssc-row'; btn.dataset.id = friend.steamid; const img = document.createElement('img'); img.className = 'ssc-av'; img.alt = ''; if (/^https:\/\//.test(friend.avatar || '')) img.src = friend.avatar; const mid = document.createElement('div'); mid.className = 'ssc-mid'; const name = document.createElement('div'); name.className = 'ssc-name'; name.textContent = friend.personaname || friend.steamid; const sub = document.createElement('div'); sub.className = 'ssc-sub'; sub.textContent = statusText(friend); mid.appendChild(name); mid.appendChild(sub); const meta = document.createElement('div'); meta.className = 'ssc-meta'; meta.appendChild(pill(daysText(friend), isFresh(friend) ? 'ssc-pill ssc-new' : 'ssc-pill')); if (friend.countryCode) meta.appendChild(pill(friend.countryCode, 'ssc-pill')); const level = levels[friend.steamid]; const levelPill = pill(level == null ? '等级 —' : ('Lv.' + level), 'ssc-pill ssc-lv'); meta.appendChild(levelPill); const ban = vac[friend.steamid]; if (ban && (ban.vac || ban.gameBan || ban.communityBanned)) meta.appendChild(pill('VAC', 'ssc-pill ssc-vac')); const mutual = core.getMutual(friend.steamid); if (mutual && !mutual.private) meta.appendChild(pill('共同 ' + mutual.count, 'ssc-pill')); btn.appendChild(img); btn.appendChild(mid); btn.appendChild(meta); return btn; } function pill(label, cls) { const node = document.createElement('span'); node.className = cls; node.textContent = label; return node; } function statusText(friend) { if (friend.personastate > 0 && friend.gameextrainfo) return '游戏中 · ' + friend.gameextrainfo; if (friend.personastate > 0) return '在线'; return '离线'; } function daysText(friend) { const days = core.pure.friendDays(friend.friend_since, Date.now()); return days == null ? '好友天数未知' : (days + ' 天'); } function isFresh(friend) { return core.pure.isNewFriend(friend, Date.now()); } function onRowClick(e) { const row = e.target.closest('.ssc-row'); if (!row) return; const friend = view.find((item) => item.steamid === row.dataset.id); if (!friend) return; host.openFriend(friend.steamid, friend.personaname || '', friend.avatar || ''); } async function refreshRoster(manual) { if (!safeKey()) { toast('需要 API Key 才能刷新名单', 'warn'); return; } const btn = $('#ssc-refresh'); if (btn) btn.disabled = true; try { await core.refreshRoster(); const list = $('#ssc-list'); const top = list ? list.scrollTop : 0; paintKpi(); paintList(true); if (list) list.scrollTop = top; drawWindow(); core.startVacFill(onVac); if (manual) toast('好友名单已更新', 'info'); } catch (e) { const code = e && e.code; if (code === 'NO_KEY') toast('需要 API Key', 'warn'); else if (code === 'NO_STEAMID') toast('没有识别到你的 SteamID', 'error'); else toast('好友名单更新失败', 'error'); } finally { if (btn) btn.disabled = false; } } function onVac() { if (!opened || !rootEl) return; paintKpi(); const vac = core.getVac(); rootEl.querySelectorAll('.ssc-row').forEach((row) => { const ban = vac[row.dataset.id]; const old = row.querySelector('.ssc-vac'); const show = ban && (ban.vac || ban.gameBan || ban.communityBanned); if (show && !old) row.querySelector('.ssc-meta').appendChild(pill('VAC', 'ssc-pill ssc-vac')); if (!show && old) old.remove(); }); } function onLevelClick() { if (!safeKey()) { toast('补齐等级需要 API Key', 'warn'); return; } if (!core.loadRoster()) { toast('先有好友名单才能补等级', 'warn'); return; } const state = core.levelJobState(); if (state.running && !state.paused) core.pauseLevelJob(); else if (state.paused) core.resumeLevelJob(); else core.startLevelJob(onLevelItem); syncLevelButton(); } function onLevelItem(id, level) { if (!id || !rootEl) return; const row = rootEl.querySelector('.ssc-row[data-id="' + cssEscape(id) + '"] .ssc-lv'); if (row) row.textContent = 'Lv.' + level; syncLevelButton(); } function cssEscape(value) { return window.CSS && CSS.escape ? CSS.escape(value) : String(value).replace(/"/g, ''); } function syncLevelButton() { const state = core.levelJobState(); const btn = $('#ssc-level'); if (!btn) return; if (state.running && !state.paused) btn.textContent = '暂停等级'; else if (state.paused) btn.textContent = '继续等级'; else btn.textContent = '补齐等级'; text('#ssc-level-note', state.total ? ('等级 ' + state.done + ' / ' + state.total) : ''); } function onIndexClick() { if (!safeKey()) { toast('补齐索引需要 API Key', 'warn'); return; } if (!core.loadRoster()) { toast('先有好友名单才能索引游戏库', 'warn'); return; } const state = core.libraryJobState(); if (state.running && !state.paused) core.pauseLibraryIndexJob(); else if (state.paused) core.resumeLibraryIndexJob(); else { const started = core.startLibraryIndexJob(onIndex); if (!started.running && !started.total) toast('游戏库索引已经齐了', 'info'); } syncIndexButton(); } function onIndex(state) { if (!rootEl) return; const stats = core.indexStats(); text('#ssc-index-count', '已索引 ' + stats.indexed + ' / ' + stats.total + (state.failed ? ' · 跳过 ' + state.failed : '')); syncIndexButton(); if (state.phase === 'done' && page === 'rank') { const current = rootEl.querySelector('[data-rank].ssc-on'); renderRank(current ? current.dataset.rank : 'days'); } if (state.phase === 'done' && page === 'compare') fillCompareSources(); } function syncIndexButton() { const state = core.libraryJobState(); const btn = $('#ssc-index'); const stats = core.indexStats(); if (!btn) return; if (state.running && !state.paused) btn.textContent = '暂停索引'; else if (state.paused) btn.textContent = '继续索引'; else btn.textContent = '补齐索引'; const note = $('#ssc-index-count'); if (note && !state.running) note.textContent = '已索引 ' + stats.indexed + ' / ' + stats.total; } function renderRank(kind) { const data = core.rank(kind || 'days'); const box = $('#ssc-rank'); box.textContent = ''; const stats = core.indexStats(); const indexedKinds = kind === 'recent2w' || kind === 'gameCount' || kind === 'playtime'; if (indexedKinds) { text('#ssc-index-count', '已索引 ' + data.indexed + ' / ' + data.total); } else if (!core.libraryJobState().running) { text('#ssc-index-count', '已索引 ' + stats.indexed + ' / ' + stats.total); } if (!data.rows.length) { const empty = document.createElement('div'); empty.className = 'ssc-empty'; empty.textContent = indexedKinds ? '还没有可排的索引数据。点「补齐索引」才会请求游戏库。' : '还没有好友名单。'; box.appendChild(empty); return; } data.rows.forEach((row, index) => { const line = document.createElement('div'); line.className = 'ssc-rank-row'; const rankNo = document.createElement('b'); rankNo.textContent = String(index + 1); const name = document.createElement('span'); name.textContent = row.name; const label = document.createElement('span'); label.className = 'ssc-note'; label.textContent = row.label; const openBtn = document.createElement('button'); openBtn.type = 'button'; openBtn.className = 'ssc-btn'; openBtn.textContent = '查看'; openBtn.addEventListener('click', () => host.openFriend(row.steamid, row.name, row.avatar || '')); line.appendChild(rankNo); line.appendChild(name); line.appendChild(label); line.appendChild(openBtn); box.appendChild(line); }); } function indexedPeople() { const roster = core.loadRoster(); const people = []; const self = host.getMySteamId(); if (self && core.peekGames(self)) { people.push({ steamid: self, personaname: '我', avatar: (roster && roster.own && roster.own.avatar) || '' }); } ((roster && roster.friends) || []).forEach((friend) => { if (core.peekGames(friend.steamid)) people.push(friend); }); return people; } function fillCompareSources() { const people = indexedPeople(); const self = host.getMySteamId(); const pair = $('#ssc-pair'); const many = $('#ssc-many'); pair.textContent = ''; many.textContent = ''; const friends = people.filter((person) => person.steamid !== self); if (!friends.length) { const opt = document.createElement('option'); opt.textContent = '还没有已索引的好友'; opt.value = ''; pair.appendChild(opt); } friends.forEach((friend) => { const opt = document.createElement('option'); opt.value = friend.steamid; opt.textContent = friend.personaname || friend.steamid; pair.appendChild(opt); }); people.forEach((person) => { const label = document.createElement('label'); const input = document.createElement('input'); input.type = 'checkbox'; input.value = person.steamid; input.checked = comparePick.indexOf(person.steamid) !== -1; input.addEventListener('change', () => { const checked = Array.from(many.querySelectorAll('input:checked')).map((node) => node.value); if (checked.length > 4) { input.checked = false; toast('最多选择 4 人', 'warn'); return; } comparePick = checked; }); label.appendChild(input); label.appendChild(document.createTextNode(' ' + (person.personaname || person.steamid))); many.appendChild(label); }); if (!people.length) { const tip = document.createElement('div'); tip.className = 'ssc-note'; tip.textContent = '索引完成后,这里才会出现可以比对的人。'; many.appendChild(tip); } } function renderPair() { const friendId = $('#ssc-pair').value; const out = $('#ssc-pair-out'); out.textContent = ''; const self = host.getMySteamId(); if (!self || !core.peekGames(self)) { out.textContent = '你的游戏库还没有索引。到排行页点「补齐索引」,或先打开一次我的游戏库。'; return; } if (!friendId) { out.textContent = '选择一个已索引的好友。'; return; } const cmp = core.comparePair(self, friendId); if (!cmp) { out.textContent = '这个好友还没有游戏库索引。'; return; } out.appendChild(statLine('都有 ' + cmp.sharedCount + ' · 我独有 ' + cmp.myOnlyCount + ' · 他独有 ' + cmp.friendOnlyCount)); out.appendChild(statLine('总时长 ' + hours(cmp.myTotalMinutes) + ' / ' + hours(cmp.friendTotalMinutes))); cmp.shared.slice().sort((a, b) => (b.myPlaytime + b.friendPlaytime) - (a.myPlaytime + a.friendPlaytime)).slice(0, 30) .forEach((game) => out.appendChild(statLine(game.name + ' · 我 ' + hours(game.myPlaytime) + ' / 他 ' + hours(game.friendPlaytime)))); } function renderMany() { const out = $('#ssc-many-out'); out.textContent = ''; const ids = comparePick.slice(0, 4); if (ids.length < 2) { out.textContent = '至少选择 2 人。'; return; } const cmp = core.compareMany(ids); if (cmp.missing && cmp.missing.length) { out.appendChild(statLine('有 ' + cmp.missing.length + ' 人没有索引,未计入。')); } out.appendChild(statLine('共同游戏 ' + cmp.count)); (cmp.games || []).slice(0, 30).forEach((game) => { const bits = (cmp.people || []).map((person) => (person.name + ' ' + hours(game.playtimes[person.steamid] || 0))); out.appendChild(statLine(game.name + ' · ' + bits.join(' / '))); }); } function renderGameSuggest() { const q = $('#ssc-game-q').value; const box = $('#ssc-game-suggest'); box.textContent = ''; core.searchGames(q, 8).forEach((game) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ssc-btn'; btn.textContent = game.name + ' (' + game.appid + ')'; btn.addEventListener('click', () => renderGameRank(game.appid)); box.appendChild(btn); }); } function renderGameRank(appid) { const data = core.rankGame(appid); const out = $('#ssc-game-out'); out.textContent = ''; out.appendChild(statLine((data.name || ('App ' + appid)) + ' · 已索引 ' + data.indexed + ' / ' + data.total)); if (!data.rows.length) { out.appendChild(statLine('已索引的好友里没有人拥有这款游戏。')); return; } data.rows.slice(0, 40).forEach((row, index) => { out.appendChild(statLine((index + 1) + '. ' + row.name + ' · ' + hours(row.minutes))); }); } function statLine(value) { const node = document.createElement('div'); node.textContent = value; return node; } function hours(minutes) { return (Math.round((minutes || 0) / 6) / 10) + 'h'; } root.SteamSocialCenter = { version: VERSION, apiVersion: API_VERSION, init, open, close, isOpen, }; })(typeof window !== 'undefined' ? window : globalThis);