// ==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 = `
填写 Steam Web API Key 后才能读取好友名单。Key 在好友列表的设置里保存,这里不会发请求。
已索引 0 / 0
游戏数、总时长、近两周只统计已经索引的好友。索引不会在打开时自动开始。
`;
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);