// ==UserScript==
// @name Steam 游戏库展示
// @namespace steam-game-library-viewer
// @author Github: SmallRob,SteamID: Healer007
// @version 2.11.47
// @description 在 Steam 商店/社区页面增加中央游戏库模态面板(橱窗/个人展柜/时长/洞察/家庭组/系列分类/绝版/愿望单/PS会免/Epic赠送/年度大作/锁区/DLC收藏,支持卡片/横版封面/列表三种视图)和右侧个人信息面板侧滑栏(概览/洞察/社交/勋章/设置)
// @license MIT
// @match https://store.steampowered.com/*
// @match https://steamcommunity.com/id/*/home/
// @match https://steamcommunity.com/id/*/home
// @match https://steamcommunity.com/profiles/*/home/
// @match https://steamcommunity.com/profiles/*/home
// @connect api.steampowered.com
// @connect store.steampowered.com
// @connect steamcommunity.com
// @connect cdn.akamai.steamstatic.com
// @connect cdn.cloudflare.steamstatic.com
// @connect shared.akamai.steamstatic.com
// @connect shared.fastly.steamstatic.com
// @connect fastly.jsdelivr.net
// @connect jsdelivr.net
// @connect cdn.jsdelivr.net
// @connect leanisssharedstorage.blob.core.windows.net
// @connect api.augmentedsteam.com
// @connect open.er-api.com
// @connect flagcdn.com
// @connect api.isthereanydeal.com
// @connect api.cheapshark.com
// @connect steamcardexchange.net
// @connect api.deepseek.com
// @connect api.openai.com
// @connect open.bigmodel.cn
// @connect dashscope.aliyuncs.com
// @connect api.moonshot.cn
// @connect api.minimaxi.com
// @connect ark.cn-beijing.volces.com
// @connect playstation.com
// @connect store.playstation.com
// @connect bartervg.com
// @connect raw.githubusercontent.com
// @connect gamestatus.info
// @connect steam-tracker.com
// @resource gameDb https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/game-db-v1.2.json
// @resource delistedData https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/steam_delisted_apps.json
// @resource psplusData https://fastly.jsdelivr.net/gh/oXnMe/psplus-steam-overlay@main/data/psplus-games.json
// scriptcat https://scriptcat.org/scripts/code/7432/SGLV%20Core%20Library.user.js?version=1.0.6
// v2.11.36: 依赖 sglv-core.lib.js v1.0.6(v1.0.5 语义不变 + gmFetchText headers 透传 + gmFetchJson onHttpError;发布顺序:先发布 core 1.0.6)
// @require https://update.greasyfork.org/scripts/589437/1909161/SGLV%20Core%20Library%20%28SGLV-Suite%29.js
// @require https://update.greasyfork.org/scripts/588483/1902911/SGLV%20%E5%85%B1%E4%BA%AB%E6%A0%B7%E5%BC%8F%20%28Library%29.js
// @require https://update.greasyfork.org/scripts/589870/1896461/SGLV%20%E4%BA%91%E5%AD%98%E6%A1%A3%E6%A8%A1%E5%9D%97%20%28Library%29.js
// @require https://update.greasyfork.org/scripts/589926/1909126/SGLV%20%E5%B0%81%E9%9D%A2%E4%B8%89%E7%BA%A7%E9%99%8D%E7%BA%A7%20%28Library%29.js
// (v2.11.44-hotfix) 以下原 [at]require 588484/1912157 sglv-game-collection 1.9.47 已改为内嵌,见下方 inline 块;ScriptCat/Tampermonkey 解析器不会把含 [at] 的注释行当作 @require,故这里用普通注释行(原 @require URL 仅作存档): 1912157 / SGLV 游戏收藏模块 (Library)
// @require https://update.greasyfork.org/scripts/590084/1909121/SGLV%20App%20Detail%20Library.js
// @require https://update.greasyfork.org/scripts/591126/1901189/SGLV%20Currency%20Library%20%28SGLV-Suite%29.js
// @require https://update.greasyfork.org/scripts/590086/1895071/SGLV%20%E6%8B%BC%E9%9F%B3%E5%AD%97%E5%BA%93%20%28Library%29.js
// @require https://update.greasyfork.org/scripts/590371/1896551/SGLV%20Badge%20Library.js
// @require https://update.greasyfork.org/scripts/590418/1896925/SGLV%20ITAD%20Library.js
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_listValues
// @grant GM_notification
// @grant unsafeWindow
// @run-at document-idle
// @tag Steam
// @tag games
// @icon data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%238b5cf6'/%3E%3Cstop offset='0.5' stop-color='%233b82f6'/%3E%3Cstop offset='1' stop-color='%2306b6d4'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='url(%23g)'/%3E%3Cg fill='%23fff'%3E%3Crect x='13' y='13' width='17' height='17' rx='3.5' opacity='.96'/%3E%3Crect x='34' y='13' width='17' height='17' rx='3.5' opacity='.78'/%3E%3Crect x='13' y='34' width='17' height='17' rx='3.5' opacity='.78'/%3E%3Crect x='34' y='34' width='17' height='17' rx='3.5' opacity='.96'/%3E%3C/g%3E%3Cpath d='M38 39 L47 44.5 L38 50 Z' fill='url(%23g)'/%3E%3C/svg%3E
// ==/UserScript==
// ============================================================================
// v2.11.44-hotfix: 内嵌 sglv-game-collection 1.9.47(原 @require 改为内联)
// ============================================================================
// 原因: 在受限网络环境下(CDN 受限/离线/GFW),ScriptCat 等脚本管理器的外部
// require 静默失败,导致 window.SGLVCollection 缺失,主脚本打印
// [SGLV] 收藏模块库未加载 警告后,收藏功能(供应商/绝版/PS会免/Epic 赠送/
// 锁区/年度大作/DLC 收藏/捆绑包)完全不可用。
// 修复: 将收藏模块库 1.9.47 直接内嵌到主脚本,window.SGLVCollection 在脚本
// 启动时即可用,init(ctx) 握手正常完成,不再依赖外部 require URL。
// 副作用: 主脚本体积增加约 446KB(1.7MB → ~2.1MB),换取完全自包含,无网络
// 依赖。如未来需切回外部 require 模式,删除本块并恢复上方 @require 行即可。
// ============================================================================
window.SGLVCollection = (function () {
'use strict';
// ---- 桥接宿主(由 init(ctx) 注入) ----
let Host = null;
const apiVersion = 1;
// ---- SGLVCore 依赖(sglv-core.lib.js 提供的零业务基础设施;缺失时所有调用走 noop 兜底) ----
// v1.9.1: DLC 渐进式加载复用 makeThrottledProgress / concurrentPool,避免每批数据都触发主线程渲染风暴
const _C = (typeof unsafeWindow !== 'undefined' && unsafeWindow.SGLVCore) || window.SGLVCore || null;
const _throttled = _C && typeof _C.makeThrottledProgress === 'function' ? _C.makeThrottledProgress : null;
const _StageProgress = _C && _C.StageProgress ? _C.StageProgress : null;
// ---- v1.9.41: 封面链语言变体增强(sglv-cover-fallback.lib.js v1.2.0 可选依赖)----
// ---- v1.9.42: 竖版 2x 默认 + 横版 hero 高清 2x 增强 ----
// 惰性解析:cover-fallback 与本库的加载顺序不作假设,每次调用时取
function _coverFb() {
try {
return (typeof unsafeWindow !== 'undefined' && unsafeWindow.SGLVCoverFallback)
|| window.SGLVCoverFallback || null;
} catch (e) { return null; }
}
// 中文界面请求语言变体封面(library_600x900_schinese 等),其余界面用默认资产
function coverLang() {
return (Host && Host.isZh) ? 'schinese' : null;
}
// v1.9.42: 4 CDN 前缀(顺序与 sglv-cover-fallback.lib.js 一致:shared.akamai → shared.fastly → cdn.akamai → cloudflare)
const _COVER_CDNS = [
'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/',
'https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/',
'https://cdn.akamai.steamstatic.com/steam/apps/',
'https://cdn.cloudflare.steamstatic.com/steam/apps/',
];
// v1.9.42: 横版 hero 高清 2x 增强链(library_hero_2x 3840x1240 → library_hero 1920x620)
// cover-fallback 可用 → 复用其 hero 链(中文语言变体 library_hero_schinese_2x 置顶 + 2x 先于非 2x + 4CDN + header 兜底);
// 不可用 → 手工构建同序链,保证可选依赖缺失时横版增强不降级
function heroEnhancedChain(appid) {
try {
const fb = _coverFb();
if (fb && typeof fb.buildFullChain === 'function'
&& Array.isArray(fb.KINDS) && fb.KINDS.indexOf('hero') >= 0) {
const chain = fb.buildFullChain(appid, 'hero', coverLang());
if (Array.isArray(chain) && chain.length) return chain;
}
} catch (e) { /* fallthrough */ }
const lang = coverLang();
const files = [];
if (lang) files.push('library_hero_' + lang + '_2x.jpg', 'library_hero_' + lang + '.jpg');
files.push('library_hero_2x.jpg', 'library_hero.jpg');
const out = [];
for (const f of files) for (const cdn of _COVER_CDNS) out.push(cdn + appid + '/' + f);
return out;
}
// v1.9.42: 链去重(保留首次出现顺序)
function _dedupeChain(chain) {
const seen = new Set();
const out = [];
for (const u of chain) if (!seen.has(u)) { seen.add(u); out.push(u); }
return out;
}
// v1.9.42: hero 前缀链去除尾部 header 兜底(header 统一留在整链末尾,
// 保证 capsule_467x181 等横版资产的优先级高于 header)
function _heroPrefixChain(appid) {
return heroEnhancedChain(appid).filter(u => u.slice(-11) !== '/header.jpg');
}
// 封面完整回退链:cover-fallback 链(steamui 精确 URL 语境 + 语言变体置顶 + 4CDN)
// → 降级 Host.coverChainUrls(v1.9.40 原行为)
// v1.9.42: ① poster 竖版链保持 2x 置顶(library_600x900_2x 高清竖版为默认:
// cover-fallback 链 / Host 链均保证 2x 先于 1x,数据源 poster 字段同为 2x URL)
// ② capsule 横版链增强:头部插入 hero 高清 2x 增强链(library_hero_2x 为默认横版封面),
// 原 capsule_467x181 链降级为 hero 缺失时的回退
function coverChainLang(appid, kind) {
try {
const fb = _coverFb();
if (fb && typeof fb.buildFullChain === 'function'
&& Array.isArray(fb.KINDS) && fb.KINDS.indexOf(kind) >= 0) {
let chain = fb.buildFullChain(appid, kind, coverLang());
if (kind === 'capsule') chain = _heroPrefixChain(appid).concat(chain);
if (Array.isArray(chain) && chain.length) return _dedupeChain(chain);
}
} catch (e) { /* fallthrough */ }
const host = Host.coverChainUrls(appid, kind);
return kind === 'capsule' ? _dedupeChain(_heroPrefixChain(appid).concat(host)) : host;
}
// good 缓存:v1.9.42 起 capsule 横版优先 steamui hero 精确 hash URL(image2x 优先,即 hero 高清 2x),
// 其次宿主会话级缓存,最后 cover-fallback 的本 kind steamui 精确 hash URL(同步缓存命中)
// v1.9.44: 交叉缓存方向门控——所有读取均校验 URL 方向与 kind 匹配(poster=竖版,其余=横版),
// 方向不匹配的缓存条目(poster 缓存被横版兜底写入、capsule 缓存混入竖版)不再直接返回,
// 防止竖版视图直用横版缓存地址导致裁切错位(或反向);横版替代走 coverHorizontalFallbackUrl
function _sgcvUrlMatchesKind(u, kind) {
if (typeof u !== 'string' || !u) return false;
const v = _sgcvIsVerticalUrl(u);
return (kind === 'poster') ? v : !v;
}
function coverGoodUrl(appid, kind) {
if (kind === 'capsule') {
try {
const fb = _coverFb();
if (fb && typeof fb.getSteamuiGoodUrl === 'function') {
const heroUrl = fb.getSteamuiGoodUrl(appid, 'hero', coverLang());
if (heroUrl && _sgcvUrlMatchesKind(heroUrl, kind)) return heroUrl;
}
} catch (e) { /* fallthrough */ }
}
const good = Host.getPosterGood(appid, kind);
if (good && _sgcvUrlMatchesKind(good, kind)) return good;
try {
const fb = _coverFb();
if (fb && typeof fb.getSteamuiGoodUrl === 'function') {
const u = fb.getSteamuiGoodUrl(appid, kind, coverLang()) || '';
if (u && _sgcvUrlMatchesKind(u, kind)) return u;
}
} catch (e) { /* fallthrough */ }
return '';
}
// v1.9.44: 竖版缺失判定——poster 缓存存在横版条目 = 此前 poster 链已耗尽至横版兜底
// (poster 链竖版优先,poster 上下文加载出横版 ⇒ 竖版资产确认缺失的会话级标记)
function coverVerticalMissing(appid) {
try {
const p = Host.getPosterGood(appid, 'poster');
return !!(p && !_sgcvIsVerticalUrl(p));
} catch (e) { return false; }
}
// v1.9.44: 横版替代交叉缓存——仅竖版确认缺失(poster 缓存横版标记条目)时返回该标记条目
// (链尾 header 兜底,2.14:1 最贴竖版容器 letterbox),未确认缺失时一律为空:
// capsule/header 缓存存在 ≠ 竖版资产缺失(横竖是各自独立文件),未确认即交叉读取
// 会令竖版视图跳过竖版链探测、直接加载错位横图
function coverHorizontalFallbackUrl(appid) {
try {
const p = Host.getPosterGood(appid, 'poster');
return (p && !_sgcvIsVerticalUrl(p)) ? p : '';
} catch (e) { return ''; }
}
// ---- v1.9.43: 封面方向门控 + API 兜底竞速 ----
// 竖版资产判定:library_600x900 族(含语言/2x 变体);header/capsule/hero/page_bg 均为横版。
// 数据源 poster 字段实测全为横版 header URL,v1.9.30-1.9.42 将其当作竖版初始 src
// 导致 2:3 竖版卡片显示横图裁切变形
function _sgcvIsVerticalUrl(u) {
return typeof u === 'string' && u.indexOf('library_600x900') >= 0;
}
// API 兜底竞速:首个 onerror 即后台拉起 appdetails(Host.fetchCoverApiUrl 三级缓存 +
// 并发去重),CDN 链继续串行走;API 先返回且图片尚未加载成功时直接劫持 src。
// 新游戏(资产仅存在于 hash 路径、无 hash 链全 404)从 ~36 次串行 404(约 22s)
// 缩短到 ~2-3 次请求;老游戏链首即命中,竞速不触发任何请求
function _sgcvCoverRaceImpl(img, appid, kind) {
if (!img || img._sgcvRace) return;
img._sgcvRace = 1;
try {
if (!Host || typeof Host.fetchCoverApiUrl !== 'function') return;
Host.fetchCoverApiUrl(appid, kind).then(function (url) {
if (!url || !img.isConnected || img.src === url) return;
// v1.9.44: 方向门控——API URL 与 kind 方向不匹配时不劫持。
// poster kind 的 API 兜底(filters=basic)恒为横版 header_image:劫持会压制
// 链上更深处仍可能命中的竖版资产,并把横版写入 poster 缓存造成
// "有竖版资产的游戏显示横版"的会话级污染;横版兜底由链尾 header + _sglvPosterFallback 承担
if (!_sgcvUrlMatchesKind(url, kind)) return;
// CDN 链已加载成功(onload 已加 sglv-img-loaded 类)则不干预
if (img.classList.contains('sglv-img-loaded') || (img.complete && img.naturalWidth > 0)) return;
// API URL 失败 → 占位(先替换 onerror,防止链式 handler 死循环)
img.onerror = function () { img.onerror = null; _sgcvToPlaceholder(img); };
img.src = url;
}).catch(function () { /* API 失败:CDN 链继续走,不干预 */ });
} catch (e) { /* Host 契约缺失:降级为纯链行走 */ }
}
// 竞速 URL 加载失败的占位(与宿主 _posterImgToPlaceholder 同款,仅替换 img 节点保留徽章浮层)
function _sgcvToPlaceholder(img) {
if (!img || !img.parentElement) return;
const holder = document.createElement('div');
holder.className = img.className;
holder.style.cssText = 'display:flex;align-items:center;justify-content:center;color:#5a6a7a;font-size:11px';
holder.textContent = (Host && Host.isZh) ? '无图片' : 'No Image';
img.parentElement.replaceChild(holder, img);
}
// 挂载到页面上下文(内联 onerror 属性在页面上下文执行,与宿主 _sglvPosterFallback 同机制)
try {
const _sgcvPageWin = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
_sgcvPageWin._sgcvCoverRace = _sgcvCoverRaceImpl;
} catch (e) { /* ignore */ }
// v1.9.44: poster kind 通用 onload 属性——宽图检测(横版兜底/替代 letterbox 显示)+ good 记录
function _sgcvPosterOnloadAttr(appid) {
return 'this.classList.add(\'sglv-img-loaded\');'
+ 'if(this.naturalWidth>this.naturalHeight)this.classList.add(\'sglv-img-wide\');'
+ '_sglvRecordPosterGood(\'' + appid + '\',this.src,\'poster\');';
}
// v1.9.44: 竖版缺失标记快速路径 HTML——竖版确认缺失后横版替代直用(交叉缓存),
// 免去每次渲染重爬整条竖版链(8 个 CDN 404 + header 兜底)
function _sgcvHorizontalFallbackImgHtml(appid, name, cls, fbFn) {
const url = coverHorizontalFallbackUrl(appid);
if (!url) return '';
return ' ';
}
// v2.9.22: sgc-panel 紧凑 CSS(仿主面板 v2.9.22 页头风格 + 品牌图标 + 拖动 + 骨架占位)
const SGC_PANEL_CSS = `
/* === 紧凑 header:去除"游戏收藏"标题,保留左侧 Steam 图标 + 中间 tab-bar + 右侧关闭 === */
.sgc-panel > .sglv-header {
padding: 8px 14px !important;
gap: 12px;
cursor: move; /* header 作为拖动手柄 */
user-select: none;
background: linear-gradient(135deg, rgba(27, 40, 56, 0.95) 0%, rgba(42, 71, 94, 0.95) 100%) !important;
border-bottom: 1px solid rgba(102, 192, 244, 0.18) !important;
}
.sgc-panel > .sglv-header h2 { display: none !important; }
.sgc-panel > .sglv-header > .sglv-tab-bar {
display: flex;
align-items: center;
gap: 2px;
flex: 1;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
}
.sgc-panel > .sglv-header > .sglv-tab-bar::-webkit-scrollbar { display: none; }
/* === 保留最左侧 Steam 图标作为品牌标识 === */
.sglv-header-brand {
width: 24px;
height: 24px;
border-radius: 6px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(102, 192, 244, 0.18), rgba(139, 92, 246, 0.18));
border: 1px solid rgba(102, 192, 244, 0.25);
color: #66c0f4;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
}
.sglv-header-brand:hover { transform: rotate(-8deg) scale(1.05); box-shadow: 0 2px 8px rgba(102, 192, 244, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.1); }
.sglv-header-brand img { width: 14px; height: 14px; display: block; border-radius: 2px; }
/* === tab 按钮紧凑化(覆盖 shared css 的 flex:1 + 大 padding) === */
.sgc-panel .sglv-tab {
flex: 0 0 auto !important;
padding: 5px 12px !important;
font-size: 12.5px !important;
font-weight: 600 !important;
border-radius: 8px !important;
border: 1px solid transparent !important;
background: transparent !important;
color: #94a3b8 !important;
white-space: nowrap !important;
gap: 5px !important;
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.15s !important;
}
.sgc-panel .sglv-tab:hover { color: #e2e8f0 !important; background: rgba(255, 255, 255, 0.06) !important; }
.sgc-panel .sglv-tab.active {
color: #fff !important;
background: rgba(102, 192, 244, 0.22) !important;
border-color: rgba(102, 192, 244, 0.4) !important;
box-shadow: 0 0 0 1px rgba(102, 192, 244, 0.15) inset, 0 1px 4px rgba(102, 192, 244, 0.18) !important;
}
.sgc-panel .sglv-tab svg { width: 14px; height: 14px; flex-shrink: 0; }
/* === 隐藏原 .sglv-tabs 单独行(兼容老代码,防止双重显示) === */
.sgc-panel .sglv-tabs { display: none !important; }
/* === 关闭按钮位置 === */
.sgc-panel > .sglv-header > .sglv-header-actions {
gap: 6px;
flex-shrink: 0;
margin-left: auto;
}
/* === v2.9.22: Epic免费标签 → 封面右下角(card + cover 两种视图) === */
.sglv-card-img-wrap > .sglv-epic-free-badge,
.sglv-wl-cover-img-wrap > .sglv-epic-free-badge {
position: absolute !important;
bottom: 6px !important;
right: 6px !important;
top: auto !important;
left: auto !important;
margin-top: 0 !important;
background: #ff69b4 !important;
color: #fff !important;
padding: 2px 6px !important;
border-radius: 3px !important;
font-size: 10px !important;
font-weight: 700 !important;
z-index: 2 !important;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35) !important;
backdrop-filter: blur(4px) !important;
-webkit-backdrop-filter: blur(4px) !important;
}
/* === v2.9.22: GotY"年度大作"标签 → 封面右下角(card + cover)== 风格与 Epic免费一致 === */
.sglv-card-img-wrap > .sglv-goty-goty-badge,
.sglv-wl-cover-img-wrap > .sglv-goty-goty-badge {
position: absolute !important;
bottom: 6px !important;
right: 6px !important;
top: auto !important;
left: auto !important;
margin-top: 0 !important;
background: linear-gradient(135deg, #f59e0b, #ef4444) !important;
color: #fff !important;
padding: 2px 6px !important;
border-radius: 3px !important;
font-size: 10px !important;
font-weight: 700 !important;
z-index: 2 !important;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35) !important;
backdrop-filter: blur(4px) !important;
-webkit-backdrop-filter: blur(4px) !important;
}
/* === v2.9.22: cover 视图下方信息行统一间距(Epic / Delisted / GotY 一致) === */
.sglv-wl-cover-body .sglv-epic-owned-row,
.sglv-wl-cover-body .sglv-blocked-owned-row,
.sglv-wl-cover-body .sglv-goty-owned-row {
margin-top: 2px;
gap: 4px;
}
/* === v2.9.22: 供应商列表视图封面列 === */
.sglv-pub-list-col.cover { padding-right: 8px; }
.sglv-pub-list-cover {
width: 80px;
height: 36px;
object-fit: cover;
border-radius: 4px;
display: block;
background: rgba(255, 255, 255, 0.05);
}
/* === v1.9.16: 供应商/系列游戏 genres 类型标签(可选字段,无 genres 时不渲染) === */
.sglv-pub-genre-tags {
display: inline-flex;
gap: 3px;
margin-left: 6px;
vertical-align: 1px;
flex-wrap: wrap;
}
.sglv-pub-genre-tag {
font-size: 10px;
line-height: 1;
padding: 2px 5px;
border-radius: 3px;
background: rgba(102, 192, 244, 0.12);
color: #9ecfea;
white-space: nowrap;
}
.sglv-card-genre-row {
display: flex;
gap: 3px;
flex-wrap: wrap;
margin-top: 3px;
}
/* === v2.9.22: GotY 状态行紧凑布局(owned badge + 🕐 发售日期同行) === */
.sglv-goty-status-row {
display: flex !important;
align-items: center !important;
gap: 6px !important;
flex-wrap: wrap !important;
margin-top: 4px !important;
min-height: 18px !important;
}
.sglv-goty-status-row .sglv-card-badge {
display: inline-flex !important;
align-items: center !important;
font-size: 10px !important;
padding: 2px 7px !important;
margin-top: 0 !important;
flex-shrink: 0 !important;
}
.sglv-goty-date {
display: inline-flex !important;
align-items: center !important;
gap: 3px !important;
font-size: 10px !important;
color: var(--sglv-text-secondary, #94a3b8) !important;
font-variant-numeric: tabular-nums !important;
margin-left: auto !important;
white-space: nowrap !important;
}
.sglv-goty-date .sglv-goty-clock {
width: 11px !important;
height: 11px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
opacity: 0.6 !important;
flex-shrink: 0 !important;
}
.sglv-goty-date .sglv-goty-clock svg {
width: 11px !important;
height: 11px !important;
}
/* list 视图中的 goty-date 样式(不要 right-push,跟其他 meta 元素 inline) */
.sglv-list-meta .sglv-goty-date {
margin-left: 0 !important;
gap: 3px !important;
}
/* === v2.9.22: PS会免封面视图 — tier-badge 绝对定位到 cover 视图封面右上角 === */
/* shared CSS 的 .sglv-card-img-wrap .sglv-ps-tier-badge 定位只对 card 视图生效;
cover 视图用 .sglv-wl-cover-img-wrap,需要独立覆盖。 */
.sglv-wl-cover-img-wrap > .sglv-ps-tier-badge {
position: absolute !important;
top: 6px !important;
right: 6px !important;
z-index: 2 !important;
backdrop-filter: blur(4px) !important;
-webkit-backdrop-filter: blur(4px) !important;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35) !important;
}
/* === v2.9.22: PS会免 cover 视图 — 信息行紧凑到 3 行内 === */
/* 状态徽章 + 在库徽章合并到同一行(左右排列),月份/版本单独一行 */
.sglv-wl-cover-body .sglv-ps-status-row {
display: flex !important;
align-items: center !important;
gap: 5px !important;
flex-wrap: wrap !important;
margin-top: 2px !important;
}
.sglv-wl-cover-body .sglv-ps-status-row .sglv-card-badge {
display: inline-flex !important;
align-items: center !important;
font-size: 10px !important;
padding: 2px 7px !important;
margin-top: 0 !important;
}
/* 月份 + 版本 meta 行紧凑 */
.sglv-wl-cover-body .sglv-ps-meta-row {
display: flex !important;
align-items: center !important;
gap: 6px !important;
flex-wrap: wrap !important;
font-size: 10px !important;
color: var(--sglv-text-secondary) !important;
}
.sglv-wl-cover-body .sglv-ps-meta-row .sglv-ps-month-item {
display: inline-flex !important;
align-items: center !important;
gap: 3px !important;
}
.sglv-wl-cover-body .sglv-ps-meta-row .sglv-ps-month-item .sglv-ps-clock {
width: 11px !important;
height: 11px !important;
display: inline-flex !important;
align-items: center !important;
opacity: 0.65 !important;
}
.sglv-wl-cover-body .sglv-ps-meta-row .sglv-ps-month-item .sglv-ps-clock svg {
width: 11px !important;
height: 11px !important;
}
.sglv-wl-cover-body .sglv-ps-meta-row .sglv-ps-versions {
display: inline-flex !important;
gap: 3px !important;
}
.sglv-wl-cover-body .sglv-ps-meta-row .sglv-ps-versions span {
font-size: 9px !important;
padding: 1px 4px !important;
border-radius: 3px !important;
background: rgba(255, 255, 255, 0.08) !important;
border: 1px solid rgba(255, 255, 255, 0.15) !important;
}
/* === v2.9.22: 年度大作 chip 强化 — 默认全部展开,active 更明显 === */
.sgc-panel .sglv-goty-year-bar {
padding: 8px 12px !important;
gap: 6px !important;
background: linear-gradient(180deg, rgba(139, 92, 246, 0.08), transparent) !important;
border-bottom: 1px solid rgba(139, 92, 246, 0.18) !important;
}
.sgc-panel .sglv-goty-year-chip {
padding: 4px 12px !important;
font-size: 12px !important;
}
.sgc-panel .sglv-goty-year-chip.active {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.4), rgba(59, 130, 246, 0.25)) !important;
color: #fff !important;
border-color: rgba(139, 92, 246, 0.6) !important;
box-shadow: 0 0 12px rgba(139, 92, 246, 0.35) !important;
}
/* === v2.9.22: PS / Epic 占位封面 logo 尺寸(适配新版 viewBox 122.88x95.18 和 100x100) === */
.sglv-ps-cover-placeholder .sglv-ps-logo {
width: 96px !important;
height: 74px !important;
opacity: 0.9 !important;
color: #fff !important;
flex-shrink: 0 !important;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.35));
}
.sglv-ps-cover-placeholder .sglv-ps-logo svg { width: 100% !important; height: 100% !important; display: block; }
/* Epic 占位容器:覆盖 PS 的蓝色渐变,改为深色盾形风格 */
.sglv-epic-cover-placeholder {
background: linear-gradient(135deg, #0d0d0d 0%, #1f1f1f 55%, #2a2a2a 100%) !important;
box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.5) !important;
}
.sglv-epic-cover-placeholder .sglv-epic-logo {
width: 96px !important;
height: 96px !important;
opacity: 1 !important;
flex-shrink: 0 !important;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.6));
}
.sglv-epic-cover-placeholder .sglv-epic-logo svg { width: 100% !important; height: 100% !important; display: block; }
/* === 加载中骨架:body 为空或仅含简单文字时显示 spinner === */
@keyframes sgc-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.sgc-body-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 320px;
padding: 40px 20px;
gap: 14px;
color: #94a3b8;
font-size: 13px;
text-align: center;
}
.sgc-body-loading .sgc-bl-spinner {
width: 36px;
height: 36px;
border: 3px solid rgba(102, 192, 244, 0.15);
border-top-color: #66c0f4;
border-radius: 50%;
animation: sgc-spin 0.9s linear infinite;
}
.sgc-body-loading .sgc-bl-title {
font-size: 15px;
font-weight: 600;
color: #cbd5e1;
}
.sgc-body-loading .sgc-bl-tip {
font-size: 12px;
color: #64748b;
line-height: 1.6;
max-width: 380px;
}
/* body 完全为空时(首次打开/切 tab 瞬间)显示 fallback 提示 */
.sgc-panel .sglv-body:empty::before {
content: "加载中…";
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #94a3b8;
font-size: 13px;
}
/* === v1.9.1: DLC 收藏标签页骨架(KPI shimmer + 卡片占位 + 阶段进度条) === */
@keyframes sglv-skel-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.sglv-skel-line {
background: linear-gradient(90deg,
rgba(102,192,244,0.06) 0%,
rgba(102,192,244,0.20) 50%,
rgba(102,192,244,0.06) 100%);
background-size: 200% 100%;
animation: sglv-skel-shimmer 1.4s ease-in-out infinite;
border-radius: 4px;
height: 14px;
margin: 6px 0;
}
.sglv-skel-line.lg { height: 22px; width: 55%; }
.sglv-skel-line.md { height: 14px; width: 40%; }
.sglv-skel-line.sm { height: 10px; width: 28%; }
.sglv-trend-kpi-card.skeleton { pointer-events: none; opacity: 0.55; }
.sglv-trend-kpi-card.skeleton .sglv-skel-line { margin: 8px 0; }
.sglv-skel-card {
background: rgba(255,255,255,0.04) !important;
border: 1px solid rgba(255,255,255,0.06) !important;
position: relative;
overflow: hidden;
}
.sglv-skel-card::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg,
transparent 0%,
rgba(102,192,244,0.10) 50%,
transparent 100%);
background-size: 200% 100%;
animation: sglv-skel-shimmer 1.4s ease-in-out infinite;
pointer-events: none;
}
.sglv-skel-card .sglv-card-img-wrap,
.sglv-skel-card .sglv-card-body { visibility: hidden; }
.sglv-skel-stage {
margin: 14px 18px;
display: flex;
flex-direction: column;
gap: 8px;
}
.sglv-skel-stage-bar {
height: 4px;
background: rgba(102,192,244,0.12);
border-radius: 2px;
overflow: hidden;
}
.sglv-skel-stage-fill {
height: 100%;
background: linear-gradient(90deg, #66c0f4, #a78bfa);
border-radius: 2px;
transition: width 0.4s ease;
}
.sglv-skel-stage-text {
font-size: 12px;
color: #94a3b8;
display: flex;
align-items: center;
gap: 8px;
}
.sglv-skel-stage-text::before {
content: "";
width: 12px;
height: 12px;
border: 2px solid rgba(102,192,244,0.25);
border-top-color: #66c0f4;
border-radius: 50%;
animation: sgc-spin 0.9s linear infinite;
flex-shrink: 0;
}
.sglv-skel-grid { padding: 0 12px 12px; }
/* === v1.9.4: DLC 加载等待动画(参考 steam-friend-manager spinner 风格) === */
.sglv-dlc-loading { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:48px 20px; gap:14px; color:#94a3b8; min-height:280px; }
.sglv-dlc-loading .sglv-dlc-spin { width:32px; height:32px; animation:sgc-spin 1s linear infinite; color:#a78bfa; }
.sglv-dlc-loading-text { font-size:13px; font-weight:600; color:#cbd5e1; }
.sglv-dlc-loading-sub { font-size:11px; color:#64748b; }
.sglv-dlc-loading-bar { width:220px; height:3px; background:rgba(102,192,244,0.12); border-radius:2px; overflow:hidden; }
.sglv-dlc-loading-bar-fill { height:100%; background:linear-gradient(90deg,#66c0f4,#a78bfa); border-radius:2px; transition:width 0.4s ease; }
/* === v1.9.4: DLC 工具栏分隔符 + 已拥有筛选 === */
.sglv-toolbar-sep { width:1px; height:24px; background:rgba(148,163,184,0.2); flex-shrink:0; margin:0 2px; }
.sglv-dlc-own-filter { display:flex; gap:2px; flex-shrink:0; }
.sglv-dlc-own-filter .sglv-btn { padding:4px 10px; font-size:11.5px; }
/* === v1.9.35: 系列详情工具栏——flex-wrap 防溢出 + 拥有状态切换标签 === */
.sglv-pub-detail-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 12px;
margin-bottom: 8px;
font-size: 13px;
color: #8a9ba8;
}
.sglv-pub-completion { flex-shrink: 0; }
.sglv-pub-own-filter { display: flex; gap: 2px; flex-shrink: 0; }
.sglv-pub-own-filter .sglv-btn { padding: 3px 10px; font-size: 11.5px; }
.sglv-pub-sort-group { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.sglv-pub-detail-toolbar .sglv-view-toggle { margin-left: auto; gap: 4px !important; }
/* === v1.9.1: DLC 收藏标签页 KPI 紧凑布局(5 列一行, 缩 padding/font, 避免换行) === */
/* shared-css 默认 .sglv-trend-kpi-row 是 4 列,5 个 KPI 必然换行 → 用 id 选择器精准覆盖 */
#sglv-dlc-kpi .sglv-trend-kpi-row {
grid-template-columns: repeat(5, 1fr);
gap: 6px;
margin-bottom: 8px;
}
#sglv-dlc-kpi .sglv-trend-kpi-card {
padding: 5px 8px 4px;
min-width: 0;
}
#sglv-dlc-kpi .sglv-trend-kpi-header {
gap: 3px;
margin-bottom: 1px;
}
#sglv-dlc-kpi .sglv-trend-kpi-icon {
width: 16px;
height: 16px;
border-radius: 4px;
}
#sglv-dlc-kpi .sglv-trend-kpi-icon svg {
width: 10px;
height: 10px;
}
#sglv-dlc-kpi .sglv-trend-kpi-label {
font-size: 10px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#sglv-dlc-kpi .sglv-trend-kpi-value {
font-size: 17px;
line-height: 1.15;
white-space: nowrap;
}
#sglv-dlc-kpi .sglv-trend-kpi-sub {
font-size: 9px;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* === v1.9.1: DLC groupMode=true 时,视图切换按钮置灰不可点 === */
/* 分组视图天然是 card 布局,viewMode 概念对它无意义 → 视觉禁用 + pointer-events none */
.sgc-panel .sglv-view-toggle.sglv-view-disabled {
opacity: 0.35;
pointer-events: none;
filter: grayscale(0.6);
}
.sgc-panel .sglv-view-toggle.sglv-view-disabled .sglv-btn.active {
/* 即使 active 也不应被强调,保持整体灰态 */
box-shadow: none;
}
/* === v1.8.2: 供应商/系列模式切换条(系列分类标签页侧边栏顶部) === */
.sgc-panel .sglv-pub-mode-switch {
display: flex;
gap: 4px;
padding: 8px 10px;
border-bottom: 1px solid rgba(255,255,255,0.06);
position: sticky;
top: 0;
background: rgba(20,30,42,0.96);
backdrop-filter: blur(4px);
z-index: 2;
}
.sgc-panel .sglv-pub-mode-btn {
flex: 1;
padding: 5px 10px;
font-size: 12px;
font-weight: 600;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
color: #94a3b8;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.sgc-panel .sglv-pub-mode-btn:hover { color: #e2e8f0; background: rgba(255,255,255,0.07); }
.sgc-panel .sglv-pub-mode-btn.active {
color: #fff;
background: rgba(102,192,244,0.22);
border-color: rgba(102,192,244,0.4);
box-shadow: 0 0 0 1px rgba(102,192,244,0.15) inset;
}
/* v2.9.90: 系列刷新按钮——不占 flex:1,固定宽度 */
.sgc-panel .sglv-pub-mode-btn.sglv-pub-series-refresh {
flex: 0 0 auto;
padding: 5px 8px;
color: #94a3b8;
}
.sgc-panel .sglv-pub-mode-btn.sglv-pub-series-refresh:hover { color: #66c0f4; }
.sgc-panel .sglv-pub-mode-btn.sglv-pub-series-refresh svg { width: 14px; height: 14px; }
.sgc-panel .sglv-pub-mode-btn.sglv-pub-series-refresh.sglv-spin svg {
animation: sgc-spin 0.8s linear infinite;
}
/* v1.9.47-fix: 绝版游戏刷新按钮 spinner 动画 */
#sglv-delisted-refresh.sglv-spin svg {
animation: sgc-spin 0.8s linear infinite;
}
/* v1.9.40: 刷新结果悬浮提示——绝对定位不参与工具栏 flex 布局,任何内容都不会压缩切换按钮 */
.sgc-panel .sglv-pub-refresh-toast {
position: absolute;
top: calc(100% + 4px);
right: 10px;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 10px;
font-size: 11px;
color: #cbd5e1;
background: rgba(15,23,35,0.96);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.35);
pointer-events: none;
white-space: nowrap;
}
.sglv-pub-status-ok { color: #4caf50; font-weight: 600; }
.sglv-pub-status-dim { color: #8f9bb3; }
.sglv-pub-status-err { color: #ef5350; font-weight: 600; }
/* v2.9.90: 锁区游戏刷新按钮 spinner 动画 */
#sglv-blocked-refresh.sglv-spin svg {
animation: sgc-spin 0.8s linear infinite;
}
.sglv-ps-status {
font-size: 11px;
color: #8f9bb3;
white-space: nowrap;
margin-left: 4px;
display: inline-flex;
align-items: center;
gap: 6px;
}
/* v2.9.91: 锁区游戏工具栏状态文本(更新时间 / 加载进度) */
.sglv-blocked-status {
font-size: 11px;
color: #8f9bb3;
white-space: nowrap;
margin-left: 4px;
display: inline-flex;
align-items: center;
}
.sglv-blocked-status.loading {
color: #66c0f4;
}
/* === v1.8.2: 系列模式扁平列表项(无二级分类) === */
.sgc-panel .sglv-series-flat-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 7px 12px;
font-size: 12.5px;
color: #cbd5e1;
cursor: pointer;
border-left: 2px solid transparent;
transition: background 0.15s, border-color 0.15s;
}
.sgc-panel .sglv-series-flat-item:hover { background: rgba(255,255,255,0.05); }
.sgc-panel .sglv-series-flat-item.active {
background: rgba(102,192,244,0.12);
border-left-color: #66c0f4;
color: #fff;
}
.sgc-panel .sglv-series-flat-item .sglv-series-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sgc-panel .sglv-series-empty-hint {
padding: 20px 12px;
font-size: 12px;
color: #8a9ba8;
text-align: center;
}
/* === v1.9.0: DLC 收藏标签页样式 === */
.sglv-dlc-group {
border: 1px solid rgba(102, 192, 244, 0.12);
border-radius: 8px;
margin-bottom: 8px;
overflow: hidden;
background: rgba(255, 255, 255, 0.02);
}
.sglv-dlc-group.collapsed .sglv-dlc-card-row { display: none; }
.sglv-dlc-group-head {
display: flex !important;
align-items: center !important;
gap: 8px !important;
padding: 8px 12px !important;
cursor: pointer !important;
background: rgba(102, 192, 244, 0.06) !important;
transition: background 0.15s !important;
}
.sglv-dlc-group-head:hover { background: rgba(102, 192, 244, 0.12) !important; }
.sglv-dlc-arrow { font-size: 12px; color: #94a3b8; transition: transform 0.15s; flex-shrink: 0; }
.sglv-dlc-pname { font-weight: 600; font-size: 13px; color: #e2e8f0; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sglv-dlc-meta { font-size: 11px; color: #94a3b8; flex-shrink: 0; }
.sglv-dlc-pct { font-weight: 700; font-size: 13px; color: #66c0f4; font-variant-numeric: tabular-nums; flex-shrink: 0; }
/* === v1.9.3: DLC 父游戏 group 展开后,DLC 卡片用横版 4 列 grid(替代原 8 列竖图) === */
.sglv-dlc-card-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding: 8px 12px 12px; }
.sglv-dlc-card-row .sglv-dlc-group-cover { width: 100% !important; }
@media (max-width: 1280px) { .sglv-dlc-card-row { grid-template-columns: repeat(3, 1fr) !important; } }
@media (max-width: 900px) { .sglv-dlc-card-row { grid-template-columns: repeat(2, 1fr) !important; } }
@media (max-width: 600px) { .sglv-dlc-card-row { grid-template-columns: 1fr !important; } }
.sglv-dlc-type-badge-music { background: rgba(34, 165, 247, 0.15); color: #22a5f7; border: 1px solid rgba(34, 165, 247, 0.25); padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 700; }
.sglv-dlc-type-badge-dlc { background: rgba(139, 92, 246, 0.15); color: #a78bfa; border: 1px solid rgba(139, 92, 246, 0.25); padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 700; }
.sglv-dlc-group-toggle { display: flex; gap: 2px; background: rgba(255, 255, 255, 0.06); border-radius: 8px; padding: 2px; }
.sglv-dlc-group-toggle .sglv-btn { padding: 4px 10px; font-size: 12px; font-weight: 600; border-radius: 6px; }
/* === v1.9.14: 捆绑包标签页专用样式 === */
.sglv-bundle-card { cursor: pointer; }
.sglv-bundle-dlc-list { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; }
.sglv-bundle-dlc-item { display: flex; align-items: center; gap: 6px; padding: 3px 6px; border-radius: 4px; background: rgba(255,255,255,0.04); transition: background 0.15s; cursor: pointer; }
.sglv-bundle-dlc-item:hover { background: rgba(102,192,244,0.12); }
.sglv-bundle-dlc-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; color: #94a3b8; }
.sglv-bundle-dlc-more { font-size: 10px; color: #64748b; padding: 2px 6px; font-style: italic; }
/* === v1.9.24: 锁区游戏 SVG 锁图案封面占位符 === */
.sglv-blocked-cover-placeholder {
width: 100%; height: 100%;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 40%, #0f3460 100%);
position: relative; overflow: hidden;
}
.sglv-blocked-cover-placeholder::before {
content: ''; position: absolute; inset: 0;
background: radial-gradient(ellipse at 50% 35%, rgba(239,68,68,0.12) 0%, transparent 60%);
}
.sglv-blocked-cover-placeholder::after {
content: ''; position: absolute; inset: 0;
background: repeating-linear-gradient(45deg, transparent 0, transparent 12px, rgba(239,68,68,0.03) 12px, rgba(239,68,68,0.03) 24px);
pointer-events: none;
}
.sglv-blocked-cover-placeholder .sglv-blocked-lock-svg {
width: 48px; height: 48px;
filter: drop-shadow(0 2px 8px rgba(239,68,68,0.4));
z-index: 1; flex-shrink: 0;
}
.sglv-blocked-cover-placeholder .sglv-blocked-cover-name {
margin-top: 8px; padding: 0 8px;
font-size: 11px; font-weight: 600;
color: rgba(248,113,113,0.8);
text-align: center; line-height: 1.3;
max-width: 90%; overflow: hidden;
text-overflow: ellipsis; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-box-orient: vertical;
z-index: 1;
}
/* 横版封面视图(cover)适配 */
.sglv-wl-cover-img-wrap .sglv-blocked-cover-placeholder {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
}
.sglv-wl-cover-img-wrap .sglv-blocked-cover-placeholder .sglv-blocked-lock-svg {
width: 40px; height: 40px;
}
.sglv-wl-cover-img-wrap .sglv-blocked-cover-placeholder .sglv-blocked-cover-name {
font-size: 10px;
}
/* 列表视图小图标 */
.sglv-list-icon.sglv-blocked-list-icon {
width: 45px; height: 21px;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, #1a1a2e, #0f3460);
border: 1px solid rgba(239,68,68,0.2);
border-radius: 3px;
}
.sglv-blocked-list-icon svg { width: 14px !important; height: 14px !important; }
/* === v1.9.24: KPI 卡片炫彩增强 === */
.sgc-panel .sglv-trend-kpi-card {
background: linear-gradient(135deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.01) 100%) !important;
border-width: 1px !important;
position: relative; overflow: hidden;
transition: transform 0.25s, box-shadow 0.25s, border-color 0.25s !important;
}
.sgc-panel .sglv-trend-kpi-card::after {
content: ''; position: absolute; top: 0; left: -100%;
width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.06), transparent);
transition: left 0.6s; pointer-events: none;
}
.sgc-panel .sglv-trend-kpi-card:hover::after { left: 100%; }
.sgc-panel .sglv-trend-kpi-card:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 20px rgba(0,0,0,0.3) !important;
}
.sgc-panel .sglv-trend-kpi-card.active {
box-shadow: 0 0 0 1px var(--sglv-accent) inset, 0 4px 16px rgba(102,192,244,0.2) !important;
}
/* 锁区 KPI 红色卡片增强 */
.sgc-panel .sglv-trend-kpi-card[style*="ef4444"] {
background: linear-gradient(135deg, rgba(239,68,68,0.08) 0%, rgba(239,68,68,0.02) 100%) !important;
}
.sgc-panel .sglv-trend-kpi-card[style*="ef4444"]:hover {
box-shadow: 0 4px 20px rgba(239,68,68,0.25) !important;
border-color: rgba(239,68,68,0.5) !important;
}
/* 橙色卡片增强 */
.sgc-panel .sglv-trend-kpi-card[style*="ff9800"] {
background: linear-gradient(135deg, rgba(255,152,0,0.08) 0%, rgba(255,152,0,0.02) 100%) !important;
}
.sgc-panel .sglv-trend-kpi-card[style*="ff9800"]:hover {
box-shadow: 0 4px 20px rgba(255,152,0,0.25) !important;
border-color: rgba(255,152,0,0.5) !important;
}
/* 绿色(mine)卡片增强 */
.sgc-panel .sglv-trend-kpi-card.mine {
background: linear-gradient(135deg, rgba(34,197,94,0.08) 0%, rgba(34,197,94,0.02) 100%) !important;
}
.sgc-panel .sglv-trend-kpi-card.mine:hover {
box-shadow: 0 4px 20px rgba(34,197,94,0.25) !important;
border-color: rgba(34,197,94,0.5) !important;
}
/* KPI 数值炫彩文字 */
.sgc-panel .sglv-trend-kpi-card .sglv-trend-kpi-value {
text-shadow: 0 0 12px currentColor;
filter: brightness(1.1);
}
/* === v1.9.24: 收集进度条炫彩 + 减细 === */
/* 系列分类筛选进度条减细(原 12px → 7px) */
.sgc-panel .sglv-progress-filter {
height: 7px !important;
border-radius: 4px !important;
}
.sgc-panel .sglv-progress-filter .sglv-progress-fill {
background: linear-gradient(90deg, #4caf50, #22d3ee, #66c0f4) !important;
box-shadow: 0 0 6px rgba(34,211,238,0.3) !important;
}
.sgc-panel .sglv-progress-filter .sglv-progress-fill.active {
background: linear-gradient(90deg, #22c55e, #34d399, #22d3ee) !important;
box-shadow: 0 0 8px rgba(34,211,238,0.5) !important;
}
/* 供应商/系列概览进度条也减细并炫彩 */
.sgc-panel .sglv-pub-card .sglv-progress,
.sgc-panel .sglv-series-card .sglv-progress {
height: 4px !important;
margin: 4px 0 2px !important;
}
.sgc-panel .sglv-pub-card .sglv-progress-fill,
.sgc-panel .sglv-series-card .sglv-progress-fill {
background: linear-gradient(90deg, #4caf50, #22d3ee) !important;
}
/* 概览大进度条炫彩 */
.sgc-panel .sglv-stat-row + .sglv-progress {
height: 8px !important;
border-radius: 4px !important;
}
.sgc-panel .sglv-stat-row + .sglv-progress .sglv-progress-fill {
background: linear-gradient(90deg, #4caf50, #22d3ee, #66c0f4, #a78bfa) !important;
box-shadow: 0 0 8px rgba(102,192,244,0.3) !important;
}
/* === v1.9.33: 图片加载占位背景 + 焦点可见性 === */
/* 图片容器加载背景——封面图加载前显示柔和渐变占位 */
.sgc-panel .sglv-card-img-wrap,
.sgc-panel .sglv-wl-cover-img-wrap {
background: linear-gradient(135deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0.02) 100%);
}
/* 图片加载完成淡入动画 */
.sgc-panel .sglv-card-img,
.sgc-panel .sglv-wl-cover-img {
opacity: 0; transition: opacity 0.3s ease;
}
.sgc-panel .sglv-card-img.sglv-img-loaded,
.sgc-panel .sglv-wl-cover-img.sglv-img-loaded {
opacity: 1;
}
/* v1.9.43: 竖版容器内横图 letterbox——无竖版资产的游戏(API 兜底返回横版 header)
onload 检测 naturalWidth>naturalHeight 后追加 sglv-img-wide 类,object-fit:contain
完整显示 + 渐变底,替代 object-fit:cover 的居中裁切变形 */
.sgc-panel .sglv-card-img.sglv-img-wide {
object-fit: contain !important;
background: linear-gradient(160deg, rgba(22, 32, 45, 0.88) 0%, rgba(27, 40, 56, 0.88) 100%);
}
/* 焦点可见性——所有交互元素 focus-visible */
.sgc-panel .sglv-btn:focus-visible,
.sgc-panel .sglv-view-btn:focus-visible,
.sgc-panel .sglv-sort-select:focus-visible,
.sgc-panel .sglv-search:focus-visible,
.sgc-panel .sglv-delisted-type-btn:focus-visible,
.sgc-panel .sglv-delisted-sub-btn:focus-visible {
outline: 2px solid var(--sglv-accent, #66c0f4);
outline-offset: 2px;
}
/* select 暗色模式原生样式修复 */
.sgc-panel .sglv-sort-select {
background-color: rgba(62, 68, 84, 0.8);
color: #c7d5e0;
border: 1px solid rgba(255,255,255,0.1);
}
/* === v1.9.34: 系列横版封面视图增强效果 === */
/* 封面卡片 hover 光晕 + 缩放 */
.sgc-panel .sglv-pub-cover-card {
transition: transform 0.28s ease, box-shadow 0.28s ease, border-color 0.28s ease !important;
}
.sgc-panel .sglv-pub-cover-card:hover {
transform: translateY(-3px) !important;
box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(102,192,244,0.15) !important;
border-color: rgba(102,192,244,0.35) !important;
}
/* 已拥有游戏封面卡片——绿色边框微光 */
.sgc-panel .sglv-pub-cover-card.sglv-pub-cover-owned {
border-color: rgba(76,175,80,0.2) !important;
}
.sgc-panel .sglv-pub-cover-card.sglv-pub-cover-owned:hover {
border-color: rgba(76,175,80,0.5) !important;
box-shadow: 0 6px 24px rgba(0,0,0,0.4), 0 0 12px rgba(76,175,80,0.15) !important;
}
/* 封面图片 hover 缩放 */
.sgc-panel .sglv-pub-cover-card .sglv-wl-cover-img {
transition: opacity 0.3s ease, transform 0.35s ease !important;
}
.sgc-panel .sglv-pub-cover-card:hover .sglv-wl-cover-img {
transform: scale(1.04) !important;
}
/* 封面底部渐变遮罩——提升文字可读性 */
.sgc-panel .sglv-pub-cover-overlay {
position: absolute !important;
bottom: 0 !important; left: 0 !important; right: 0 !important;
height: 50% !important;
background: linear-gradient(to top, rgba(0,0,0,0.55) 0%, transparent 100%) !important;
pointer-events: none !important;
z-index: 1 !important;
}
/* 拥有状态徽章——封面右上角毛玻璃 */
.sgc-panel .sglv-pub-cover-badge {
position: absolute !important;
top: 6px !important; right: 6px !important;
z-index: 2 !important;
margin-top: 0 !important;
backdrop-filter: blur(6px) !important;
-webkit-backdrop-filter: blur(6px) !important;
box-shadow: 0 1px 4px rgba(0,0,0,0.35) !important;
}
/* === v1.9.33: prefers-reduced-motion 无障碍降级 === */
@media (prefers-reduced-motion: reduce) {
.sgc-panel *,
.sgc-panel *::before,
.sgc-panel *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.sgc-panel .sglv-card-img,
.sgc-panel .sglv-wl-cover-img {
opacity: 1 !important;
transition: none !important;
}
}
`;
// v1.9.24: 锁区游戏 SVG 锁图案——用于标签页图标和 KPI 卡片图标
// 全新样式:渐变锁体 + 弧形锁扣 + 钥匙孔,带红色警示色调
const BLOCKED_LOCK_ICON_SVG = ''
+ ' '
+ ' '
+ ' '
+ ' '
+ ' ';
// v1.9.24: 锁区游戏 SVG 锁图案——最终兜底占位符
// 使用递增序号生成唯一 gradient ID 避免多实例冲突
let _blockedCoverSeq = 0;
function blockedCoverSvg() {
const sid = '_blk' + (_blockedCoverSeq++);
return ''
+ ''
+ ''
+ ' '
+ ' '
+ ''
+ ' '
+ ' '
+ ''
+ ' '
+ ' '
+ ' '
+ ' '
+ ' '
+ ' '
+ ' '
+ ' '
+ ' '
+ ' ';
}
// v1.9.24: 锁区游戏封面兜底 DOM(CDN + API 全部失败时展示)
function blockedPlaceholderDom(name) {
const holder = document.createElement('div');
holder.className = 'sglv-blocked-cover-placeholder';
holder.innerHTML = blockedCoverSvg() + '
' + Host.wlEscape(name || '') + '
';
return holder;
}
// v1.9.24: 锁区游戏封面全局回退——Steam CDN 全部失败时先用 Store API 取真实封面 URL,
// API URL 仍加载失败时替换为 SVG 锁占位符(参照 _sglvPsCoverFallback 逻辑)
// v1.9.29: 传递 kind 参数,使 API 兜底按图片类型选择最佳 URL
unsafeWindow._sglvBlockedCoverFallback = function(img, appid, kind) {
if (!img || !img.parentElement) return;
Host.fetchCoverApiUrl(appid, kind).then(url => {
if (url) {
img.onerror = function() { img.onerror = null; img.parentElement.replaceChild(blockedPlaceholderDom(img.alt), img); };
img.src = url;
return;
}
img.parentElement.replaceChild(blockedPlaceholderDom(img.alt), img);
});
};
// v1.9.24: 锁区游戏列表视图图标回退——CDN 图标失败时替换为锁图标占位 div
unsafeWindow._sglvBlockedListIconFallback = function(img) {
if (!img || !img.parentElement) return;
const holder = document.createElement('div');
holder.className = 'sglv-list-icon sglv-blocked-list-icon';
holder.innerHTML = ' ';
img.parentElement.replaceChild(holder, img);
};
// v1.9.24: 锁区游戏竖版封面——Steam 多 CDN 回退链 + Store API + SVG 锁兜底
// 参照 psplusPosterImg 逻辑:先查 good 缓存 → CDN 回退链 → API → 锁占位
// v1.9.29: ① 传递 kind 给 _sglvBlockedCoverFallback ② onerror 链跳过 chain[0](与初始 src 相同)
// v1.9.41: good/链走 coverGoodUrl/coverChainLang(steamui 精确 URL + 语言变体链,见模块头注释)
function blockedPosterImg(appid, name) {
const good = coverGoodUrl(appid, 'poster');
if (good) {
return ' ';
}
// v1.9.44: 竖版确认缺失 → 横版替代直用(交叉缓存)
const horizHtml = _sgcvHorizontalFallbackImgHtml(appid, Host.wlEscape(name), 'sglv-card-img', '_sglvBlockedCoverFallback');
if (horizHtml) return horizHtml;
const chain = coverChainLang(appid, 'poster');
let handler = '_sglvBlockedCoverFallback(this, \'' + appid + '\', \'poster\');';
for (let i = chain.length - 1; i >= 1; i--) {
handler = 'this.src=\'' + chain[i] + '\';this.onerror=function(){' + handler + '}';
}
handler = '_sgcvCoverRace(this, \'' + appid + '\', \'poster\');' + handler;
return ' ';
}
// v1.9.24: 锁区游戏横版封面(capsule 大横幅)——同上逻辑,kind=capsule
// v1.9.29: ① 传递 kind 给 _sglvBlockedCoverFallback ② onerror 链跳过 chain[0]
// v1.9.41: good/链走 coverGoodUrl/coverChainLang
function blockedCapsuleImg(appid, name) {
const good = coverGoodUrl(appid, 'capsule');
if (good) {
return ' ';
}
const chain = coverChainLang(appid, 'capsule');
let handler = '_sglvBlockedCoverFallback(this, \'' + appid + '\', \'capsule\');';
for (let i = chain.length - 1; i >= 1; i--) {
handler = 'this.src=\'' + chain[i] + '\';this.onerror=function(){' + handler + '}';
}
handler = '_sgcvCoverRace(this, \'' + appid + '\', \'capsule\');' + handler;
return ' ';
}
// v2.9.22: sgc-panel 拖动(参考 steam-friend-manager 1.2.5 makeDraggable)
// 用 !important 覆盖 shared CSS 中 sglv-panel 的 top/left/transform 定位
function makeSgcDraggable(el, handle) {
if (!el || !handle) return;
let isDragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0;
const onDown = (e) => {
const t = e.target;
if (t.closest('button, input, select, textarea, label, a')) return;
isDragging = true;
startX = e.clientX; startY = e.clientY;
const rect = el.getBoundingClientRect();
startLeft = rect.left; startTop = rect.top;
el.style.cursor = 'grabbing';
e.preventDefault();
};
const onMove = (e) => {
if (!isDragging) return;
el.style.setProperty('left', (startLeft + e.clientX - startX) + 'px', 'important');
el.style.setProperty('top', (startTop + e.clientY - startY) + 'px', 'important');
el.style.setProperty('right', 'auto', 'important');
el.style.setProperty('bottom', 'auto', 'important');
el.style.setProperty('transform', 'none', 'important');
};
const onUp = () => {
if (!isDragging) return;
isDragging = false;
el.style.cursor = '';
};
handle.addEventListener('mousedown', onDown);
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
function init(ctx) {
if (!ctx || typeof ctx !== 'object') {
console.warn('[SGLVCollection] init: invalid ctx');
return false;
}
if (ctx.apiVersion !== apiVersion) {
console.warn('[SGLVCollection] Host apiVersion mismatch: expected', apiVersion, 'got', ctx.apiVersion);
// 尝试用宿主的 showToast 提示(可能不可用)
try {
if (ctx.showToast) {
const zh = ctx.isZh;
ctx.showToast(zh
? '收藏模块版本不兼容,请更新主脚本或收藏模块'
: 'Collection module version mismatch, please update host script or collection module');
}
} catch (e) { /* ignore */ }
return false;
}
Host = ctx;
// v1.9.47-fix: 同步预热绝版游戏远程源 TTL 缓存(避免开 tab 才发起请求,首屏秒开)
try { _warmDelistedRemoteCache(); } catch (e) { /* 忽略 */ }
return true;
}
// ==================== 游戏收藏模块 ====================
// v1.9.5: 永久缓存层——绕过宿主 TTL 缓存
// v1.9.6: 从 GM_getValue/GM_setValue 迁移到 SGLVCore.IDB
// 原因:GM_setValue 在多数环境有 5MB 限制,大收藏数据(数千条 + 元数据)可能撑爆
// IDB 容量无限制,且与主脚本 PCC 持久化层同源,统一管理
// 旧 key 'sglv_perm_xxx' 兼容:首次读取时尝试从旧 GM key 迁移,迁移后清旧 key
const PERM_CACHE_PREFIX = 'sglv_idb_perm_';
const PERM_CACHE_GM_PREFIX = 'sglv_perm_'; // v1.9.5 旧 key,用于一次性迁移
const _C_IDB = _C && _C.IDB;
function _permCacheGet(key) {
// 1) 优先从 IDB 读
if (_C_IDB) {
try {
const v = _C_IDB.get(PERM_CACHE_PREFIX + key, null);
if (v != null) return v;
} catch (e) { /* IDB 读失败,降级到旧 GM */ }
}
// 2) 旧 GM 兜底(一次性迁移)— 读旧 key 并迁移到 IDB
try {
const raw = GM_getValue(PERM_CACHE_GM_PREFIX + key, '');
if (!raw) return null;
const parsed = JSON.parse(raw);
// 立即迁移到 IDB
if (_C_IDB && parsed != null) {
try { _C_IDB.set(PERM_CACHE_PREFIX + key, parsed); } catch (e) { /* ignore */ }
// 迁移成功后清旧 key(可选,这里保留以防 IDB 写入失败)
// 不主动清,让 IDB 读优先 + 下次读时也会再次尝试迁移
}
return parsed;
} catch { return null; }
}
function _permCacheSet(key, data) {
if (_C_IDB) {
try { _C_IDB.set(PERM_CACHE_PREFIX + key, data); return; } catch (e) { /* IDB 失败,降级 */ }
}
// 降级到旧 GM
try { GM_setValue(PERM_CACHE_GM_PREFIX + key, JSON.stringify(data)); } catch (e) { /* ignore quota */ }
}
// v1.9.5: 按 appid 增量合并——新数据覆盖同 appid 条目,旧数据中独有的条目保留
function _mergeByAppid(oldItems, newItems) {
if (!Array.isArray(oldItems) || oldItems.length === 0) return newItems || [];
if (!Array.isArray(newItems) || newItems.length === 0) return oldItems;
const map = new Map();
oldItems.forEach(g => { if (g && g.appid) map.set(g.appid, g); });
newItems.forEach(g => { if (g && g.appid) map.set(g.appid, g); });
return Array.from(map.values());
}
// v1.9.5: appid 数组增量合并(去重)
function _mergeAppidArray(oldIds, newIds) {
const set = new Set(oldIds || []);
(newIds || []).forEach(id => set.add(id));
return Array.from(set);
}
// v1.9.5: 后台刷新节流检查——intervalMs 内已刷新过则跳过
function _shouldThrottle(lastTs, intervalMs) {
return lastTs > 0 && (Date.now() - lastTs) < intervalMs;
}
// v1.9.5: debounce 工具函数——避免搜索框每次按键触发完整重渲染
function _debounce(fn, delay = 200) {
let timer = null;
const debounced = function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay);
};
return debounced;
}
// v1.9.5: 缓存 Intl.Collator 实例,避免每次过滤都重新构造
const _nameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
let _collectionActiveTab = 'delisted'; // v2.9.22: 默认标签改为"绝版游戏"(与新 tab 顺序一致)
let _collectionBodyInitialized = false; // v2.9.22: 跟踪 body 是否已首次渲染——避免首次打开闪一帧骨架
let collectionOverlayEl = null;
let collectionPanelEl = null;
// ---------- 模块本地状态(v2.6.4 起不再挂主 state,库化后天然隔离) ----------
// v1.8.2: mode = 'publisher' | 'series' — 供应商/系列双模式切换(系列数据独立展示不合并)
// v2.9.92: 新增 supplierResult/seriesResult 跟踪各自刷新结果(updated/nochange/error)
// v1.9.40: 移除 supplierFetchedAt/seriesFetchedAt——更新时间文本不再常驻工具栏(曾压缩切换按钮致变形),
// 刷新结果改为悬浮提示(不参与 flex 布局)
const pubState = { mode: 'publisher', expandedPublishers: new Set(), selectedPublisher: null, selectedSeries: null, viewMode: 'card', filter: 'all',
dataLoading: false, refreshStage: '',
supplierResult: '', seriesResult: '', showResultUntil: 0,
// v1.9.31: 系列详情排序——sortField: order|date|name|appid,sortDir: asc|desc
sortField: 'order', sortDir: 'asc' };
// v1.9.19: 系列详情分页状态(供应商/系列详情共用)——pageSize 与其他 tab 保持一致(48)
// pageCache: { key: 页码 } 记忆每个系列的浏览页码,切换回已浏览分类时恢复进度
const pubDetailState = { page: 1, pageSize: 48, cacheKey: null, pageCache: new Map() };
const delistedState = { search: '', typeFilter: 'all', subFilter: 'all', page: 1, pageSize: 48, viewMode: 'card',
// v1.9.32: 绝版游戏排序——appidDesc|appidAsc|dateDesc|dateAsc
// v1.9.47-fix: pageSize 50→48——与其他 tab(psplus/epic/blocked/goty/dlc/bundle)保持一致
sort: 'appidDesc' };
const psplusState = { games: [], loaded: false, loading: false, search: '', tierFilter: 'all', statusFilter: 'all', sort: 'dateDesc', page: 1, pageSize: 48, viewMode: 'card', updatedAt: '', loadError: null };
// v2.7.9: Epic 赠送模块状态(数据仅为 Steam appid 数组,名称异步加载)
const epicState = { appids: [], loaded: false, loading: false, search: '', ownFilter: 'all', sort: 'appidDesc', page: 1, pageSize: 48, viewMode: 'card', updatedAt: '', loadError: null, source: '' };
// v2.9.7: 年度大作模块状态(数据含 name/date/appid,名称无需异步加载)
// v1.7.0: 加 year 字段——按年拉取数据 (GitHub 2024/2025/2026.json), 默认 null = 自动选最新有数据的年份
const gotyState = { games: [], loaded: false, loading: false, year: null, availableYears: [], search: '', ownFilter: 'all', sort: 'dateDesc', page: 1, pageSize: 48, viewMode: 'card', updatedAt: '', loadError: null, source: '', lastFetchAt: 0 };
// v1.9.0: DLC 收藏模块状态——三级渐进加载(Barter.vg DLC DB → Steam 全库存 type → DLC 子类型)
// v1.9.1: 加 stage / level*Done 字段——支持 L1∥L2 并行 + 中间阶段增量 rebuild(不阻塞 UI 进程)
// v1.9.2: 加 dlcTypeCache 字段——本地 IDB 永久缓存 DLC 子类型映射(只增不减),实现真正增量
const dlcState = {
dlcs: [], parentTotalMap: {},
loaded: false, loading: false, loadError: null,
search: '', typeFilter: 'all', sort: 'parentAsc', page: 1, pageSize: 48,
viewMode: 'card', groupMode: true, expandedParents: new Set(),
ownFilter: 'all', // v1.9.4: 全部/已拥有 筛选
stage: 0, // 0=未开始 1=Barter.vg 2=appType 3=DLC 子类型
level1Done: false, // Barter.vg DLC DB 完成
level2Done: false, // Steam 全库存 appType 完成
rebuildQueued: false, // 防抖:同一帧多次 scheduleRebuild 合并
// v1.9.2: 本地 IDB 永久缓存,key=appId,value=type('dlc'/'music'/'video'...)
// 加载时用此缓存立即填充 buildDlcList,只 enrich 缓存里没有的 DLC(真正的"只增不减"增量)
dlcTypeCache: {},
dlcTypeCacheLoaded: false, // IDB 异步加载完成后置 true
idbLoadPromise: null, // 第一次 IDB.loadAll 的 promise(避免重复)
// v1.9.3: 后台静默刷新节流(避免短时间内重复 enrich)
lastBgRefreshAt: 0,
// v1.9.8: 后台 DLC 子类型增量获取中(不阻塞 UI 渲染)
dlcTypeEnriching: false,
// v1.9.8: 缓存 filteredDlc 结果(filters/search/sort 未变时跳过重算)
_filteredCache: null,
_filteredCacheKey: null,
};
// v1.9.2: IDB 缓存 schema 版本号(用于 cache invalidation 升级)
// v1.9.3: 版本号稳定为 1——业务 key(dlc_subtype_map)+ 内容 schema 都没变,lib 升级不应清缓存
const _DLC_CACHE_VERSION = { dlc_subtype: 1 };
// v1.9.14: 捆绑包标签页状态——复用 DLC 进包数据(Host.getBundleCount + Host.getDlcsByParentMap)
const bundleState = {
data: [], // [{appid,name,isOwned,totalBundle,dlcCount,dlcs:[{appid,name,bundleCount,isOwned,playtime}]}]
loaded: false,
search: '',
sort: 'bundleDesc',
page: 1,
pageSize: 48,
};
const _DLC_TYPE_CACHE_IDB_KEY = 'dlc_subtype_map';
// v1.9.3: 后台静默刷新节流——6h(与 cloud-save 同步,避免短时间内重复跑 L1+L2+L3)
const _DLC_BG_REFRESH_INTERVAL = 6 * 3600 * 1000;
// v1.9.1: DLC 三级加载阶段文案(loading 骨架中央显示当前阶段)
const DLC_STAGES = [
null, // 0 占位
{ pct: 15, zh: '加载 Barter.vg DLC 数据库…', en: 'Loading Barter.vg DLC DB…' },
{ pct: 55, zh: '补全 Steam 全库存应用类型…', en: 'Enriching Steam app types…' },
{ pct: 85, zh: '获取 DLC 子类型(音乐/视频…)…', en: 'Fetching DLC subtypes…' },
];
// ==================== 供应商分类标签页 ====================
// v1.9.19: 统计缓存——数据安装期注入后低频更新,按 GAME_DB 引用缓存计算结果;
// @resource 更新后 getGameDb() 返回新引用 → Map key 变化自动失效(Map 最多保留 2 代引用,防内存泄漏)
const _pubStatsCache = new Map(); // dbRef → { sig, inner: Map(pubName → { total, owned }) }
const _seriesStatsCache = new Map(); // dbRef → { sig, inner: Map(pubName + '\u0000' + seriesName → { total, owned }) }
function _getStatsCacheForDb(map) {
const db = Host.getGameDb();
if (!db) return null;
// v1.9.19-fix: sig = 拥有集合规模 + 家庭共享开关——面板打开期间重新同步家庭库
// (sglv:games-updated)时 db 引用不变但拥有集合变化,需整代失效;
// sig 随每代缓存独立存放(不共用全局表),避免两个缓存重建时序互相干扰
const sig = (Host.getOwnedGames ? Host.getOwnedGames().length : -1) + '|' + !!Host.getShowFamilyShared();
let entry = map.get(db);
if (entry && entry.sig !== sig) entry = null; // sig 变化 → 整代失效
if (!entry) {
entry = { sig, inner: new Map() };
map.set(db, entry);
if (map.size > 2) map.delete(map.keys().next().value);
}
return entry.inner;
}
function computePublisherStats(pubName) {
const db = Host.getGameDb();
const pub = db?.[pubName];
if (!pub) return { total: 0, owned: 0 };
const inner = _getStatsCacheForDb(_pubStatsCache);
if (inner) {
const hit = inner.get(pubName);
if (hit) return hit;
}
let total = 0, owned = 0;
const activeOwnedIds = Host.getActiveOwnedAppIds();
for (const series of Object.values(pub)) {
total += series.length;
for (const g of series) if (activeOwnedIds.has(g.id)) owned++;
}
const res = { total, owned };
if (inner) inner.set(pubName, res);
return res;
}
function computeSeriesStats(pubName, seriesName) {
const db = Host.getGameDb();
const series = db?.[pubName]?.[seriesName];
if (!series) return { total: 0, owned: 0 };
const inner = _getStatsCacheForDb(_seriesStatsCache);
const key = pubName + '\u0000' + seriesName;
if (inner) {
const hit = inner.get(key);
if (hit) return hit;
}
let owned = 0;
const activeOwnedIds = Host.getActiveOwnedAppIds();
for (const g of series) if (activeOwnedIds.has(g.id)) owned++;
const res = { total: series.length, owned };
if (inner) inner.set(key, res);
return res;
}
// v1.9.16: genres 类型标签 HTML(game-db-v1.2.json 可选字段;旧数据无 genres 时返回空串,安全降级)
function genreTagsHtml(g, max = 3) {
if (!Array.isArray(g.genres) || !g.genres.length) return '';
const tags = g.genres.slice(0, max).map(t => `${Host.wlEscape(t)} `).join('');
const more = g.genres.length > max ? `+${g.genres.length - max} ` : '';
return tags + more;
}
// 统一计算游戏拥有状态徽章信息(供供应商分类列表/卡片视图复用)
function getGameBadgeInfo(g, ownedAppIds) {
const isOwned = ownedAppIds.has(g.id);
if (!isOwned) return { isOwned: false, badgeClass: 'missing', badgeText: Host.T.missing, tooltip: '' };
const gameObj = Host.getOwnedGames().find(og => og.appid === g.id);
if (gameObj && !Host.isGameOwnedByMe(gameObj)) {
return { isOwned: true, badgeClass: 'shared', badgeText: Host.isZh ? '共享' : 'Shared', tooltip: `title="共享自: ${Host.getGameOwnerNames(gameObj)}"` };
}
return { isOwned: true, badgeClass: 'owned', badgeText: '✓ ' + Host.T.owned, tooltip: '' };
}
function renderPublisherTab(parent) {
const body = document.createElement('div');
body.className = 'sglv-pub-layout';
// 左侧边栏(v1.8.2: 顶部含供应商/系列模式切换)
const sidebar = document.createElement('div');
sidebar.className = 'sglv-pub-sidebar';
sidebar.id = 'sglv-pub-sidebar';
body.appendChild(sidebar);
// 右侧内容
const main = document.createElement('div');
main.className = 'sglv-pub-main';
main.id = 'sglv-pub-main';
body.appendChild(main);
parent.appendChild(body);
renderPubSidebar();
if (pubState.mode === 'series') {
// v1.8.2: 系列模式——默认展开第一个系列
ensureSeriesSelected();
if (pubState.selectedSeries) renderSeriesModeDetail(pubState.selectedSeries);
else renderSeriesModeEmpty();
} else {
// v1.9.19: 供应商模式——默认展开首个供应商并选中其首个系列,按需渲染(不再默认停留 Overview)
ensurePublisherSelected();
if (pubState.selectedSeries) {
renderPubSeriesDetail(pubState.selectedPublisher, pubState.selectedSeries);
} else if (pubState.selectedPublisher) {
renderPubPublisherDetail(pubState.selectedPublisher);
} else {
renderPubOverview(); // GAME_DB 为空时保持总览(空态)
}
}
}
/** v1.9.19: 确保供应商模式下有选中项:未选中/选中项已不存在时默认展开第一个供应商并选中其第一个系列(参照 ensureSeriesSelected) */
function ensurePublisherSelected() {
const db = Host.getGameDb() || {};
const pubNames = Object.keys(db);
if (!pubNames.length) { pubState.selectedPublisher = null; pubState.selectedSeries = null; return; }
if (!pubState.selectedPublisher || !db[pubState.selectedPublisher]) {
pubState.selectedPublisher = pubNames[0];
pubState.selectedSeries = null;
}
pubState.expandedPublishers.add(pubState.selectedPublisher);
if (!pubState.selectedSeries || !db[pubState.selectedPublisher][pubState.selectedSeries]) {
const seriesNames = Object.keys(db[pubState.selectedPublisher]);
pubState.selectedSeries = seriesNames.length ? seriesNames[0] : null;
}
}
// ---- v1.8.2: 系列数据访问(宿主未提供 getSeriesDb 时安全降级) ----
function getSeriesDb() {
return (typeof Host.getSeriesDb === 'function' && Host.getSeriesDb()) || {};
}
function computeSeriesModeStats(seriesName) {
const games = getSeriesDb()[seriesName] || [];
let owned = 0;
const activeOwnedIds = Host.getActiveOwnedAppIds();
for (const g of games) if (activeOwnedIds.has(g.id)) owned++;
return { total: games.length, owned };
}
/** 确保系列模式下有选中项:未选中/选中项已不存在时默认展开第一个系列 */
function ensureSeriesSelected() {
const db = getSeriesDb();
const names = Object.keys(db);
if (!names.length) { pubState.selectedSeries = null; return; }
if (!pubState.selectedSeries || !db[pubState.selectedSeries]) {
pubState.selectedSeries = names[0];
}
}
/** v1.8.2: 供应商/系列模式切换条 */
function renderPubModeSwitch() {
const wrap = document.createElement('div');
wrap.className = 'sglv-pub-mode-switch';
const modes = [
{ key: 'publisher', label: Host.isZh ? '供应商' : 'Publisher' },
{ key: 'series', label: Host.isZh ? '系列' : 'Series' },
];
for (const m of modes) {
const btn = document.createElement('button');
btn.className = 'sglv-pub-mode-btn' + (pubState.mode === m.key ? ' active' : '');
btn.textContent = m.label;
btn.addEventListener('click', () => {
if (pubState.mode === m.key) return;
pubState.mode = m.key;
pubState.filter = 'all';
renderPubSidebar();
if (m.key === 'series') {
ensureSeriesSelected();
if (pubState.selectedSeries) renderSeriesModeDetail(pubState.selectedSeries);
else renderSeriesModeEmpty();
} else {
// v1.9.19: 切回供应商模式——同样默认进入首个供应商的首个系列(选中项有效则保留)
ensurePublisherSelected();
if (pubState.selectedSeries) {
renderPubSeriesDetail(pubState.selectedPublisher, pubState.selectedSeries);
} else if (pubState.selectedPublisher) {
renderPubPublisherDetail(pubState.selectedPublisher);
} else {
renderPubOverview();
}
}
});
wrap.appendChild(btn);
}
// v2.9.91: 两种模式均显示刷新按钮——同时刷新供应商+系列数据
const hasRefresh = typeof Host.loadSeriesData === 'function' || typeof Host.loadSupplierData === 'function';
if (hasRefresh) {
const refreshBtn = document.createElement('button');
refreshBtn.className = 'sglv-pub-mode-btn sglv-pub-series-refresh';
refreshBtn.id = 'sglv-pub-refresh';
refreshBtn.title = Host.isZh ? '刷新供应商和系列数据' : 'Refresh supplier & series data';
refreshBtn.innerHTML = Host.ICONS.refresh;
refreshBtn.addEventListener('click', () => loadAllCollectionData(true));
wrap.appendChild(refreshBtn);
// v1.9.40: 不再常驻状态文本(更新时间曾压缩切换按钮致变形)——刷新结果由悬浮提示显示
}
return wrap;
}
/**
* v1.9.37: 刷新供应商/系列标签页内容(侧边栏+详情区域),不闪骨架屏
* 供 loadAllCollectionData 完成回调和 sglv:series/supplier-data-loaded 事件复用
*/
function refreshPublisherTabContent() {
if (_collectionActiveTab !== 'publisher' || !collectionPanelEl || !collectionPanelEl.classList.contains('sglv-show')) return;
renderPubSidebar();
if (pubState.mode === 'series') {
ensureSeriesSelected();
if (pubState.selectedSeries) renderSeriesModeDetail(pubState.selectedSeries);
else renderSeriesModeEmpty();
} else {
ensurePublisherSelected();
if (pubState.selectedSeries) {
renderPubSeriesDetail(pubState.selectedPublisher, pubState.selectedSeries);
} else if (pubState.selectedPublisher) {
renderPubPublisherDetail(pubState.selectedPublisher);
} else {
renderPubOverview();
}
}
}
/**
* v2.9.91: 同时加载供应商和系列数据(并行)
* v1.9.37: 移除 refreshStage 加载进度文本,仅保留刷新按钮 spinner 动画
* v2.9.92: 收集各自结果状态,刷新完成后显示汇总(已更新/已是最新/失败)
* v1.9.40: 结果汇总改为悬浮提示,不参与工具栏 flex 布局
* @param {boolean} force - 强制刷新
*/
function loadAllCollectionData(force) {
if (pubState.dataLoading) return;
pubState.dataLoading = true;
pubState.supplierResult = '';
pubState.seriesResult = '';
const refreshBtn = document.getElementById('sglv-pub-refresh');
if (refreshBtn) refreshBtn.classList.add('sglv-spin');
const tasks = [];
// 供应商数据(v1.9.37: 不再传 onProgress 回调,避免加载文本导致界面变形)
if (typeof Host.loadSupplierData === 'function') {
tasks.push(Host.loadSupplierData(force).then(result => {
pubState.supplierResult = (result && result.status) || 'error';
}).catch(() => {
pubState.supplierResult = 'error';
}));
}
// 系列数据
if (typeof Host.loadSeriesData === 'function') {
tasks.push(Host.loadSeriesData(force).then(result => {
pubState.seriesResult = (result && result.status) || 'error';
}).catch(() => {
pubState.seriesResult = 'error';
}));
}
Promise.all(tasks).finally(() => {
pubState.dataLoading = false;
if (refreshBtn) refreshBtn.classList.remove('sglv-spin');
// v2.9.92: 显示刷新结果摘要(3秒后消失)
pubState.showResultUntil = Date.now() + 3000;
renderPubRefreshToast();
setTimeout(() => {
if (pubState.showResultUntil && Date.now() >= pubState.showResultUntil) {
pubState.showResultUntil = 0;
renderPubRefreshToast();
}
}, 3100);
// v1.9.37: 使用统一的刷新函数,不再内联渲染逻辑
refreshPublisherTabContent();
});
}
/** v1.9.40: 刷新结果悬浮提示(3秒后自动消失)
* 取代 v2.9.92 的常驻状态文本——更新时间文本曾压缩供应商/系列切换按钮导致变形;
* 提示为绝对定位悬浮元素,不参与工具栏 flex 布局,任何内容都不会影响按钮宽度 */
function renderPubRefreshToast() {
const wrap = document.querySelector('.sgc-panel .sglv-pub-mode-switch');
if (!wrap) return;
let el = wrap.querySelector('.sglv-pub-refresh-toast');
// 刷新结果摘要(刷新完成后3秒内显示)
if (pubState.showResultUntil && Date.now() < pubState.showResultUntil) {
const parts = [];
const zh = Host.isZh;
// 供应商结果
if (pubState.supplierResult === 'updated') {
parts.push(`${zh ? '供应商✓' : 'Pub✓'} `);
} else if (pubState.supplierResult === 'nochange') {
parts.push(`${zh ? '供应商=' : 'Pub='} `);
} else if (pubState.supplierResult === 'error') {
parts.push(`${zh ? '供应商✗' : 'Pub✗'} `);
}
// 系列结果
if (pubState.seriesResult === 'updated') {
parts.push(`${zh ? '系列✓' : 'Ser✓'} `);
} else if (pubState.seriesResult === 'nochange') {
parts.push(`${zh ? '系列=' : 'Ser='} `);
} else if (pubState.seriesResult === 'error') {
parts.push(`${zh ? '系列✗' : 'Ser✗'} `);
}
if (!parts.length) return;
if (!el) {
el = document.createElement('span');
el.className = 'sglv-pub-refresh-toast';
wrap.appendChild(el);
}
el.innerHTML = parts.join(' ');
return;
}
pubState.showResultUntil = 0;
if (el) el.remove();
}
/** v1.8.2: 系列模式侧边栏——扁平系列列表(不做二级分类) */
function renderSeriesSidebarList(sidebar) {
const db = getSeriesDb();
const names = Object.keys(db);
// v1.9.37: 移除"系列数据加载中…"提示,数据未就绪时直接返回(避免界面变形)
if (!names.length) return;
for (const seriesName of names) {
const sStats = computeSeriesModeStats(seriesName);
const completionPct = sStats.total > 0 ? Math.round(sStats.owned / sStats.total * 100) : 0;
const item = document.createElement('div');
item.className = 'sglv-series-flat-item' + (pubState.selectedSeries === seriesName ? ' active' : '');
item.innerHTML = `
${seriesName}
${sStats.owned}/${sStats.total}
`;
item.addEventListener('click', () => {
pubState.selectedSeries = seriesName;
renderPubSidebar();
renderSeriesModeDetail(seriesName);
});
sidebar.appendChild(item);
}
}
function renderPubSidebar() {
const sidebar = document.getElementById('sglv-pub-sidebar');
if (!sidebar) return;
sidebar.innerHTML = '';
// v1.8.2: 顶部模式切换(供应商 / 系列)
sidebar.appendChild(renderPubModeSwitch());
// v1.8.2: 系列模式——扁平系列列表,无二级分类
if (pubState.mode === 'series') {
renderSeriesSidebarList(sidebar);
return;
}
for (const [pubName, pubData] of Object.entries(Host.getGameDb())) {
const pStats = computePublisherStats(pubName);
const isExpanded = pubState.expandedPublishers.has(pubName);
const pubEl = document.createElement('div');
pubEl.className = 'sglv-pub-item';
const header = document.createElement('div');
header.className = `sglv-pub-header${pubState.selectedPublisher === pubName && !pubState.selectedSeries ? ' active' : ''}`;
header.innerHTML = `
${pubName}
${pStats.owned}/${pStats.total}
`;
header.addEventListener('click', () => {
if (pubState.expandedPublishers.has(pubName)) pubState.expandedPublishers.delete(pubName);
else pubState.expandedPublishers.add(pubName);
pubState.selectedPublisher = pubName;
pubState.selectedSeries = null;
renderPubSidebar();
renderPubPublisherDetail(pubName);
});
pubEl.appendChild(header);
const seriesList = document.createElement('div');
seriesList.className = `sglv-series-list${isExpanded ? ' expanded' : ''}`;
for (const [seriesName, games] of Object.entries(pubData)) {
const sStats = computeSeriesStats(pubName, seriesName);
const completionPct = sStats.total > 0 ? Math.round(sStats.owned / sStats.total * 100) : 0;
const isSelected = pubState.selectedPublisher === pubName && pubState.selectedSeries === seriesName;
const seriesHeader = document.createElement('div');
seriesHeader.className = `sglv-series-header${isSelected ? ' active' : ''}`;
seriesHeader.innerHTML = `
${seriesName}
${sStats.owned}/${sStats.total}
`;
seriesHeader.addEventListener('click', e => {
e.stopPropagation();
pubState.selectedPublisher = pubName;
pubState.selectedSeries = seriesName;
renderPubSidebar();
renderPubSeriesDetail(pubName, seriesName);
});
seriesList.appendChild(seriesHeader);
}
pubEl.appendChild(seriesList);
sidebar.appendChild(pubEl);
}
}
function renderPubOverview() {
const main = document.getElementById('sglv-pub-main');
if (!main) return;
let totalAll = 0, ownedAll = 0;
for (const pubName of Object.keys(Host.getGameDb())) {
const s = computePublisherStats(pubName);
totalAll += s.total;
ownedAll += s.owned;
}
const pctAll = totalAll > 0 ? Math.round(ownedAll / totalAll * 100) : 0;
const showFamilyShared = Host.getShowFamilyShared();
const activeTotalGames = showFamilyShared ? Host.getOwnedGames().length : Host.getOwnedGames().filter(g => Host.isGameOwnedByMe(g)).length;
let html = `
${activeTotalGames}
${Host.T.kpiOfTotal}
${ownedAll}
${Host.T.kpiRate} ${pctAll}%
${totalAll - ownedAll}
${Host.T.kpiRate} ${100 - pctAll}%
${pctAll}%
${Host.T.kpiCollectionProgress}
`;
for (const [pubName] of Object.entries(Host.getGameDb())) {
const pStats = computePublisherStats(pubName);
const pct = pStats.total > 0 ? Math.round(pStats.owned / pStats.total * 100) : 0;
html += `
${pubName}
${pStats.owned}/${pStats.total} (${pct}%)
`;
}
main.innerHTML = html;
main.querySelectorAll('.sglv-pub-card').forEach(card => {
card.addEventListener('click', () => {
const pubName = card.dataset.pub;
pubState.expandedPublishers.add(pubName);
pubState.selectedPublisher = pubName;
pubState.selectedSeries = null;
renderPubSidebar();
renderPubPublisherDetail(pubName);
});
});
}
function renderPubPublisherDetail(pubName) {
const main = document.getElementById('sglv-pub-main');
if (!main) return;
const pStats = computePublisherStats(pubName);
const pct = pStats.total > 0 ? Math.round(pStats.owned / pStats.total * 100) : 0;
let html = `
${pubName}
${pStats.total - pStats.owned}
`;
for (const [seriesName] of Object.entries(Host.getGameDb()[pubName] || {})) {
const sStats = computeSeriesStats(pubName, seriesName);
const sPct = sStats.total > 0 ? Math.round(sStats.owned / sStats.total * 100) : 0;
html += `
${seriesName}
${sStats.owned}/${sStats.total} (${sPct}%)
`;
}
main.innerHTML = html;
main.querySelectorAll('.sglv-series-card').forEach(card => {
card.addEventListener('click', () => {
const p = card.dataset.pub, s = card.dataset.series;
pubState.selectedPublisher = p;
pubState.selectedSeries = s;
pubState.expandedPublishers.add(p);
renderPubSidebar();
renderPubSeriesDetail(p, s);
});
});
}
function renderPubSeriesDetail(pubName, seriesName) {
const games = Host.getGameDb()[pubName]?.[seriesName] || [];
const breadcrumbHtml = `
${pubName}
›
${seriesName}
`;
renderSeriesDetailCommon(breadcrumbHtml, games, () => renderPubSeriesDetail(pubName, seriesName), 'pub:' + pubName + '\u0000' + seriesName);
}
// v1.8.2: 系列模式详情——直接展示系列游戏列表(无供应商面包屑)
function renderSeriesModeDetail(seriesName) {
const games = getSeriesDb()[seriesName] || [];
const breadcrumbHtml = `
${Host.isZh ? '系列' : 'Series'}
${seriesName}
`;
renderSeriesDetailCommon(breadcrumbHtml, games, () => renderSeriesModeDetail(seriesName), 'series:' + seriesName);
}
// v1.8.2: 系列数据未加载时的右侧空态
// v1.9.37: 移除"系列数据加载中,请稍候…"提示,清空主区域即可(避免界面变形)
function renderSeriesModeEmpty() {
const main = document.getElementById('sglv-pub-main');
if (!main) return;
main.innerHTML = '';
}
/**
* v1.9.31: 系列游戏排序辅助——日期解析为可排序字符串
* 支持所有数据源日期格式(兼容 game_series.json 中文格式 + vediogame_filtered.json ISO 格式):
* "YYYY年M月D日" / "YYYY年M月" / "YYYY年"(中文格式,game_series.json)
* "YYYY-MM-DD" / "YYYY-MM-__" / "YYYY-MM" / "YYYY"(ISO 格式,vediogame_filtered.json)
* "Q[1-4] YYYY"(季度格式)
* "待定" / "TBD" / "" → 返回空串(排序时排到末尾)
* @param {string} dateStr - 日期字符串
* @returns {string} 可排序字符串(如 "20260901"),无效/未知日期返回 ''
*/
function seriesDateToSortable(dateStr) {
if (!dateStr) return '';
var s = String(dateStr).trim();
if (!s || s === '待定' || s === 'TBD' || s === 'tbd') return '';
// 中文完整日期: "2026年01月30日" / "2026 年7月8日"
var m = s.match(/(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日/);
if (m) return m[1] + m[2].padStart(2, '0') + m[3].padStart(2, '0');
// 中文年月: "2028年3月"
var m2 = s.match(/(\d{4})\s*年\s*(\d{1,2})\s*月/);
if (m2) return m2[1] + m2[2].padStart(2, '0') + '99';
// 中文年份: "2026年"
var m3 = s.match(/^(\d{4})\s*年/);
if (m3) return m3[1] + '1299';
// ISO 完整日期: "2026-09-01"
var m4 = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(s);
if (m4) return m4[1] + m4[2].padStart(2, '0') + m4[3].padStart(2, '0');
// ISO 部分日期(日未知): "2026-01-__"
var m5 = /^(\d{4})-(\d{1,2})-__$/.exec(s);
if (m5) return m5[1] + m5[2].padStart(2, '0') + '99';
// ISO 年月: "2026-08"
var m6 = /^(\d{4})-(\d{1,2})$/.exec(s);
if (m6) return m6[1] + m6[2].padStart(2, '0') + '99';
// 纯年份: "2026"
var m7 = /^(\d{4})$/.exec(s);
if (m7) return m7[1] + '1299';
// 季度格式: "Q4 2026" → 该季度末月
var m8 = /^Q([1-4])\s+(\d{4})$/i.exec(s);
if (m8) { var qMon = String(parseInt(m8[1]) * 3).padStart(2, '0'); return m8[2] + qMon + '99'; }
return '';
}
/**
* v1.9.31: 系列游戏排序——按 sortField + sortDir 排序,未知/空日期始终排到末尾
* @param {Array} games - 游戏列表 [{id, name, date, order, poster?}]
* @param {string} sortField - 'order'|'date'|'name'|'appid'
* @param {string} sortDir - 'asc'|'desc'
* @returns {Array} 排序后的新数组(不修改原数组)
*/
function sortSeriesGames(games, sortField, sortDir) {
var sorted = games.slice();
var desc = sortDir === 'desc';
try {
sorted.sort(function (a, b) {
var r;
switch (sortField) {
case 'name':
r = _nameCollator.compare(a.name || '~', b.name || '~');
return desc ? -r : r;
case 'appid':
r = (a.id || 0) - (b.id || 0);
return desc ? -r : r;
case 'date':
// 未知/空日期始终排到末尾,与 sortDir 无关
var sa = seriesDateToSortable(a.date);
var sb = seriesDateToSortable(b.date);
if (!sa && !sb) return 0;
if (!sa) return 1; // a 未知 → 排后面
if (!sb) return -1; // b 未知 → 排后面
return desc ? sb.localeCompare(sa) : sa.localeCompare(sb);
case 'order':
default:
r = (a.order || 0) - (b.order || 0);
return desc ? -r : r;
}
});
} catch (e) {
console.warn('[SGLV-Series] sortSeriesGames 排序失败,降级为原始顺序:', e);
}
return sorted;
}
/**
* v1.9.30: 系列封面图生成——优先使用数据源 poster 字段(预验证URL,避免CDN试错),
* 失败后走三级 CDN 降级链 + API 兜底。向后兼容:poster 为空时走原 Host 逻辑。
* @param {Object} g - 游戏条目 {id, name, poster?}
* @param {string} kind - 图片类型 'poster'|'header'
* @param {string} cls - img class
* @returns {string} img 标签 HTML
*/
function seriesCoverImg(g, kind, cls) {
var appid = g.id;
var name = Host.wlEscape(g.name || '');
// v1.9.43: onload 属性——竖版 kind 追加宽图检测(横图 letterbox,避免 2:3 容器裁切变形)
var onloadAttr = 'this.classList.add(\'sglv-img-loaded\');'
+ (kind === 'poster' ? 'if(this.naturalWidth>this.naturalHeight)this.classList.add(\'sglv-img-wide\');' : '')
+ '_sglvRecordPosterGood(\'' + appid + '\',this.src,\'' + kind + '\');';
// v1.9.43: poster 字段方向门控——数据源 poster 实为横版 header URL(预验证可用),
// 竖版 kind 仅接受竖版资产(library_600x900 族),横版 kind 仅接受横版 URL
var posterOk = false;
if (g.poster) posterOk = (kind === 'poster') ? _sgcvIsVerticalUrl(g.poster) : !_sgcvIsVerticalUrl(g.poster);
// v1.9.43: 首个 onerror 即竞速 API 兜底(CDN 链继续走,API 先返回则劫持 src)
var race = '_sgcvCoverRace(this,\'' + appid + '\',\'' + kind + '\');';
// 1. 优先使用 good 缓存(跨会话已验证 URL,最快)v1.9.41: 叠加 steamui 精确 URL
// v1.9.44: coverGoodUrl 已方向严格——poster kind 仅返回竖版条目
var good = coverGoodUrl(appid, kind);
if (good) {
return ' ';
}
// v1.9.44: 竖版确认缺失(poster 缓存横版标记)→ 横版替代直用(交叉缓存),免整链重爬
if (kind === 'poster' && coverVerticalMissing(appid)) {
var horizHtml = _sgcvHorizontalFallbackImgHtml(appid, name, cls, '_sglvPosterFallback');
if (horizHtml) return horizHtml;
}
// 2. 有可用 poster 字段(方向门控后):用作初始 src(预验证URL,避免CDN试错),
// onerror 走完整 CDN 链(含 chain[0],初始 src 为 g.poster)+ 竞速 API 兜底
if (posterOk) {
var chain = coverChainLang(appid, kind);
var handler = '_sglvPosterFallback(this,\'' + appid + '\',\'' + kind + '\');';
for (var i = chain.length - 1; i >= 0; i--) {
handler = 'this.src=\'' + chain[i] + '\';this.onerror=function(){' + handler + '};';
}
return ' ';
}
// 3. 无可用 poster(含横版 poster 被 poster kind 门控拒绝):链首起步 + 竞速。
// v1.9.43: 竖版 kind 不再回退 Host.posterImg,统一走本库语言变体链(2x 置顶)
var hChain = coverChainLang(appid, kind);
var hHandler = "_sglvPosterFallback(this,'" + appid + "','" + kind + "');";
for (var j = hChain.length - 1; j >= 1; j--) {
hHandler = "this.src='" + hChain[j] + "';this.onerror=function(){" + hHandler + '};';
}
return ' ';
}
// v1.9.34: 系列横版封面图(capsule kind)——用于封面视图
// v1.9.41: good/链走 coverGoodUrl/coverChainLang(steamui 精确 URL + 语言变体链)
// v1.9.43: ① 横版 poster 字段复用——数据源 poster 实为预验证横版 header URL,
// hero 链首未命中(新游戏无 hero 资产)后第 2 跳即命中,替代 ~36 次全 404 链爬取
// (460x215 与封面视图容器 aspect-ratio 完全一致);竖版 poster 仍拒绝
// ② 首个 onerror 即竞速 API 兜底
function seriesCapsuleImg(g) {
var appid = g.id;
var name = Host.wlEscape(g.name || '');
var cls = 'sglv-wl-cover-img';
// 1. 优先使用 good 缓存
var good = coverGoodUrl(appid, 'capsule');
if (good) {
return ' ';
}
// 2. hero 链首起步;横版 poster 插入为第 2 跳;其余 CDN 链跟随 + API 兜底
var chain = coverChainLang(appid, 'capsule');
var seq = chain.slice(1);
if (g.poster && !_sgcvIsVerticalUrl(g.poster) && seq.indexOf(g.poster) < 0) {
seq = [g.poster].concat(seq);
}
var handler = '_sglvPosterFallback(this,\'' + appid + '\',\'capsule\');';
for (var i = seq.length - 1; i >= 0; i--) {
handler = 'this.src=\'' + seq[i] + '\';this.onerror=function(){' + handler + '};';
}
return ' ';
}
/**
* 系列/供应商游戏列表公共渲染(KPI + 进度筛选 + 卡片/列表视图)
* @param {string} breadcrumbHtml - 顶部面包屑 HTML
* @param {Array} games - [{id, name, date}]
* @param {function} rerender - 重渲染回调
* @param {string} cacheKey - v1.9.19: 分类标识(含 filter),用于分页进度记忆/恢复
*/
function renderSeriesDetailCommon(breadcrumbHtml, games, rerender, cacheKey) {
const main = document.getElementById('sglv-pub-main');
if (!main) return;
cacheKey = cacheKey ? cacheKey + '\u0000' + pubState.filter + '\u0000' + pubState.sortField + '\u0000' + pubState.sortDir : null;
// v1.9.19: 切换分类时保存旧页码、恢复新分类页码(filter/viewMode 变化仍走 rerender,页码由 cacheKey 区分)
if (pubDetailState.cacheKey !== cacheKey) {
if (pubDetailState.cacheKey) pubDetailState.pageCache.set(pubDetailState.cacheKey, pubDetailState.page);
pubDetailState.page = (cacheKey && pubDetailState.pageCache.get(cacheKey)) || 1;
pubDetailState.cacheKey = cacheKey;
}
const ownedAppIds = Host.getActiveOwnedAppIds();
let ownedCount = 0;
for (const g of games) if (ownedAppIds.has(g.id)) ownedCount++;
const sStats = { total: games.length, owned: ownedCount };
const pct = sStats.total > 0 ? Math.round(sStats.owned / sStats.total * 100) : 0;
const viewMode = pubState.viewMode; // v1.9.34: 'card' | 'cover' | 'list'
let html = breadcrumbHtml + `
${viewMode === 'list' ? '' : viewMode === 'cover' ? '
' : '
'}
`;
// 按拥有状态筛选游戏
let displayGames = games;
if (pubState.filter === 'owned') {
displayGames = games.filter(g => ownedAppIds.has(g.id));
} else if (pubState.filter === 'unowned') {
displayGames = games.filter(g => !ownedAppIds.has(g.id));
}
// v1.9.31: 排序——按 sortField + sortDir 排序,未知/空日期排末尾
displayGames = sortSeriesGames(displayGames, pubState.sortField, pubState.sortDir);
// v1.9.19: 分页——只渲染当前页的游戏卡片/封面(页大小与其他 tab 一致)
const { pageItems, totalPages, page: _clampedPage } = Host.paginate(displayGames, pubDetailState.page, pubDetailState.pageSize);
pubDetailState.page = _clampedPage;
if (displayGames.length === 0) {
html += `
${pubState.filter === 'owned' ? (Host.isZh ? '该系列没有已拥有的游戏' : 'No owned games in this series') : pubState.filter === 'unowned' ? (Host.isZh ? '该系列没有未拥有的游戏' : 'All games in this series are owned') : (Host.isZh ? '该分类暂无游戏' : 'No games in this category')}
`;
} else if (viewMode === 'list') {
// 列表视图:表头(含封面列)
html += `
`;
for (const g of pageItems) {
const { badgeClass, badgeText, tooltip } = getGameBadgeInfo(g, ownedAppIds);
// v1.9.30: 封面优先使用数据源 poster 字段(预验证URL),失败走 CDN 降级链 + API 兜底
const coverImg = seriesCoverImg(g, 'header', 'sglv-pub-list-cover');
html += `
${coverImg}
${badgeText}
${g.name} ${(() => { const tags = genreTagsHtml(g); return tags ? `${tags} ` : ''; })()}
${g.id}
`;
}
} else if (viewMode === 'cover') {
// v1.9.34: 横版封面视图(capsule 大图)
for (const g of pageItems) {
const { isOwned, badgeClass, badgeText, tooltip } = getGameBadgeInfo(g, ownedAppIds);
html += `
${seriesCapsuleImg(g)}
${badgeText}
${g.name} ${g.id}
${(() => { const tags = genreTagsHtml(g); return tags ? `
${tags}
` : ''; })()}
`;
}
} else {
// 卡片视图
for (const g of pageItems) {
const { isOwned, badgeClass, badgeText, tooltip } = getGameBadgeInfo(g, ownedAppIds);
html += `
${seriesCoverImg(g, 'poster', 'sglv-card-img')}
${g.name}
AppID: ${g.id}
${(() => { const tags = genreTagsHtml(g); return tags ? `
${tags}
` : ''; })()}
${badgeText}
`;
}
}
html += '
';
// v1.9.19: 分页导航(与其他 tab 同一套 Host.renderPagination)
html += ``;
main.innerHTML = html;
// v2.3.33:异步加载游戏中文名
main.querySelectorAll('[data-sglv-appid]').forEach(el => {
Host.loadGameZhName(el, el.getAttribute('data-sglv-appid'), el.textContent);
});
// 统一事件绑定
// v1.9.35: 已拥有/未拥有 切换标签——点击已激活的标签则取消(回到全部)
const ownOwnedBtn = main.querySelector('#sglv-pub-own-owned');
const ownUnownedBtn = main.querySelector('#sglv-pub-own-unowned');
if (ownOwnedBtn) ownOwnedBtn.addEventListener('click', () => {
pubState.filter = pubState.filter === 'owned' ? 'all' : 'owned';
pubDetailState.page = 1;
rerender();
});
if (ownUnownedBtn) ownUnownedBtn.addEventListener('click', () => {
pubState.filter = pubState.filter === 'unowned' ? 'all' : 'unowned';
pubDetailState.page = 1;
rerender();
});
// v1.9.31: 排序事件——字段切换 + 方向切换
const sortFieldSelect = main.querySelector('#sglv-pub-sort-field');
if (sortFieldSelect) sortFieldSelect.addEventListener('change', (e) => {
pubState.sortField = e.target.value;
pubDetailState.page = 1;
rerender();
});
const sortDirBtn = main.querySelector('#sglv-pub-sort-dir');
if (sortDirBtn) sortDirBtn.addEventListener('click', (e) => {
e.stopPropagation();
pubState.sortDir = pubState.sortDir === 'asc' ? 'desc' : 'asc';
pubDetailState.page = 1;
rerender();
});
// v1.9.31: data-view 属性区分视图按钮与排序方向按钮
main.querySelectorAll('.sglv-view-btn[data-view]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
pubState.viewMode = btn.dataset.view;
rerender();
});
});
// v1.9.19: 分页事件(onJump 驱动首/上/下/末页与跳页输入)
Host.renderPagination(document.getElementById('sglv-pub-series-pagination'), 'sglv-pub-series', pubDetailState.page, totalPages, displayGames.length,
() => { pubDetailState.page--; rerender(); },
() => { pubDetailState.page++; rerender(); },
(p) => { pubDetailState.page = p; rerender(); });
}
// ==================== 绝版游戏标签页 ====================
// v1.9.47-fix: 绝版游戏数据兜底链——
// ① Host.getDelistedDb() 同步取(来自主脚本 @resource delistedData;3.5MB 大文件,Tampermonkey
// 偶发静默加载失败 + 解析慢导致"暂无绝版游戏数据"),
// ② 若 ① 为空 → 异步拉远程源三级回退(fastly.jsdelivr → cdn.jsdelivr → raw.githubusercontent),
// ③ 写回 Host.cacheSet('delistedDbRemote', ...) 供后续会话秒开(6h 节流后台刷新)。
// 选 _refreshDelistedFromRemote 而非替换 Host.getDelistedDb(闭包不可改),改为缓存到
// _delistedRemoteCache,并让 getDelistedApps() 优先取它,保留 @resource 作为快速通道。
const _DELISTED_REMOTE_SOURCES = [
'https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/steam_delisted_apps.json',
'https://cdn.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/steam_delisted_apps.json',
'https://raw.githubusercontent.com/SmallRob/steam-namespace/main/data/steam_delisted_apps.json'
];
let _delistedRemoteCache = null; // 远程源完整 JSON {version, source, fetchedAt, totalCount, data}
let _delistedRemoteLoading = false; // 防止并发请求
let _delistedRemoteLoaded = false; // 远程已成功加载过(避免每次开 tab 重新拉)
const _DELISTED_REMOTE_BG_INTERVAL = 6 * 3600 * 1000; // 6h 后台节流
let _delistedLastBgRefreshAt = 0;
let _delistedForceRemote = false; // 工具栏刷新按钮触发(force 走全链路)
function _normalizeDelistedRaw(raw) {
if (!raw || typeof raw !== 'object') return [];
let rawApps = [];
if (Array.isArray(raw.data)) rawApps = raw.data;
else if (raw.delisted && Array.isArray(raw.delisted.apps)) rawApps = raw.delisted.apps;
return rawApps.map(app => ({
appid: app.appId || app.appid,
name: app.name,
type: app.delistType || app.type || 'Delisted',
changed: app.delistedAt || app.changed,
owners: app.ownerPercentage != null ? app.ownerPercentage + '%' : (app.owners || '-'),
achievements: app.achievementCount || app.achievements,
keyshopPrice: app.keyshopPrice,
source: app.source
}));
}
// 解析远程响应文本,校验为合法绝版 JSON(data 数组 / totalCount > 0)
function _parseDelistedResponseText(txt) {
try {
const obj = JSON.parse(txt);
if (!obj || typeof obj !== 'object') return null;
const okData = Array.isArray(obj.data) && obj.data.length > 0;
const okLegacy = obj.delisted && Array.isArray(obj.delisted.apps) && obj.delisted.apps.length > 0;
if (!okData && !okLegacy) return null;
return obj;
} catch (e) { return null; }
}
function _refreshDelistedFromRemote(force) {
if (typeof GM_xmlhttpRequest !== 'function') return;
if (_delistedRemoteLoading) return;
_delistedRemoteLoading = true;
function _tryNext(idx) {
if (idx >= _DELISTED_REMOTE_SOURCES.length) {
_delistedRemoteLoading = false;
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: _DELISTED_REMOTE_SOURCES[idx],
timeout: 20000,
onload(r) {
try {
const txt = _safeRespText(r);
const obj = _parseDelistedResponseText(txt);
if (obj) {
_delistedRemoteCache = obj;
_delistedRemoteLoaded = true;
_delistedLastBgRefreshAt = Date.now();
// 写 TTL 缓存(24h),后续会话秒开
try { Host.cacheSet('delistedDbRemote', obj, 24 * 3600 * 1000); } catch (e) { /* 忽略 */ }
_delistedRemoteLoading = false;
// 触发 UI 重渲染(若当前已开 delisted tab)
// v1.9.47-fix: 数据到达时,若 wrap 还是"加载骨架占位"状态(无 toolbar/KPI),
// 清空 body 重建 tab 一次性同步 KPI/分类汇总/工具栏(否则仅刷新列表内容)。
if (_collectionActiveTab === 'delisted' && collectionPanelEl && collectionPanelEl.classList.contains('sglv-show')) {
const body = collectionPanelEl.querySelector('#sgc-body');
if (body) {
const wrap = body.querySelector('.sglv-delisted-wrap');
const hasFullUi = wrap && wrap.querySelector('#sglv-delisted-pagination');
if (hasFullUi) {
renderDelistedGamesList();
} else {
// 清空 body 重建,避免旧 wrap 与新 wrap 重复
body.innerHTML = '';
renderDelistedTab(body);
}
}
}
return;
}
} catch (e) { /* fallthrough */ }
_tryNext(idx + 1);
},
onerror() { _tryNext(idx + 1); },
ontimeout() { _tryNext(idx + 1); }
});
}
_tryNext(0);
}
// 启动时尝试从 TTL 缓存预热远程源(避免开 tab 才发起请求)
function _warmDelistedRemoteCache() {
try {
const cached = Host.cacheGet('delistedDbRemote');
if (cached && typeof cached === 'object') {
if ((Array.isArray(cached.data) && cached.data.length > 0) ||
(cached.delisted && Array.isArray(cached.delisted.apps) && cached.delisted.apps.length > 0)) {
_delistedRemoteCache = cached;
_delistedRemoteLoaded = true;
}
}
} catch (e) { /* 忽略 */ }
}
function getDelistedApps() {
// ① 优先 _delistedRemoteCache(已通过远程源/TTL 缓存预热),正常情况 @resource 加载也走这条分支
if (_delistedRemoteCache) return _normalizeDelistedRaw(_delistedRemoteCache);
// ② @resource 数据(主脚本 GM_getResourceText('delistedData') 同步返回)
const DELISTED_DB = Host.getDelistedDb();
if (DELISTED_DB && ((Array.isArray(DELISTED_DB.data) && DELISTED_DB.data.length > 0) ||
(DELISTED_DB.delisted && Array.isArray(DELISTED_DB.delisted.apps) && DELISTED_DB.delisted.apps.length > 0))) {
return _normalizeDelistedRaw(DELISTED_DB);
}
// ③ 远程源未触发过 → 触发(force 模式跳过 @resource 兜底走全链路)
if (!_delistedRemoteLoaded && !_delistedRemoteLoading) {
_refreshDelistedFromRemote(_delistedForceRemote);
}
return [];
}
// 工具栏"刷新"按钮触发的强制刷新入口
function forceRefreshDelistedFromRemote() {
_delistedForceRemote = true;
_delistedRemoteLoaded = false; // 强制重走源
_delistedRemoteCache = null;
_delistedRemoteLoading = false;
_refreshDelistedFromRemote(true);
}
function getTypeKey(type) {
if (!type) return 'delisted';
// 已经是小写下划线格式,直接返回
if (type.includes('_')) return type;
// 大写格式 -> 小写下划线格式
const map = {
'Delisted': 'delisted',
'Purchase disabled': 'purchase_disabled',
'F2P (unavailable)': 'f2p_unavailable',
'Retail only': 'retail_only',
'Test app': 'test_app',
};
return map[type] || 'delisted';
}
function getTypeLabel(type) {
if (!type) return Host.T.typeDelisted;
const map = {
'Delisted': Host.T.typeDelisted,
'delisted': Host.T.typeDelisted,
'Purchase disabled': Host.T.typePurchaseDisabled,
'purchase_disabled': Host.T.typePurchaseDisabled,
'F2P (unavailable)': Host.T.typeF2P,
'f2p_unavailable': Host.T.typeF2P,
'Retail only': Host.T.typeRetailOnly,
'retail_only': Host.T.typeRetailOnly,
'Test app': Host.T.typeTestApp,
'test_app': Host.T.typeTestApp,
};
return map[type] || type;
}
function getFilteredDelistedApps() {
const allApps = getDelistedApps();
// v2.3.14: 使用 getActiveOwnedAppIds() 替代 ownedAppIds,
// 以尊重"显示家庭共享"开关:关闭时仅匹配自己拥有的游戏,不匹配家庭组共享游戏
const ownedIds = Host.getActiveOwnedAppIds();
// v1.9.47-fix: 缓存键加入 ownedIds 集合指纹——
// 此前仅用 ownedIds.size(拥有游戏数量)做粗粒度失效,同数量但不同游戏的 owned 集合变化会导致
// subFilter=owned/missing 筛选结果过时(新购买的不展示,已退款的仍展示)。
// 用前 8 个 appid 的 djb2 哈希 + 数量作指纹,既能精确失效又避免 ownedIds 全量入键(缓存键爆炸)。
let _ownedSig = 0;
if (ownedIds && ownedIds.size) {
const sample = Array.from(ownedIds).slice(0, 8);
let h = ownedIds.size * 2654435761;
for (const n of sample) {
h = ((h << 5) + h + Number(n) | 0) >>> 0;
}
_ownedSig = h.toString(36);
}
const cacheK = Host.cacheKey('delisted-filter', allApps.length, delistedState.subFilter, delistedState.typeFilter, delistedState.search, delistedState.sort, ownedIds.size, _ownedSig);
return Host.getCached(cacheK, () => {
let apps = allApps;
// 子分类筛选:全部 / 已拥有 / 未拥有
if (delistedState.subFilter === 'owned') {
apps = apps.filter(a => ownedIds.has(Number(appidFix(a.appid))) || ownedIds.has(String(a.appid)));
} else if (delistedState.subFilter === 'missing') {
apps = apps.filter(a => !ownedIds.has(Number(appidFix(a.appid))) && !ownedIds.has(String(a.appid)));
}
// Type filter
if (delistedState.typeFilter !== 'all') {
apps = apps.filter(a => getTypeKey(a.type) === delistedState.typeFilter);
}
// Search filter
if (delistedState.search) {
const q = delistedState.search.toLowerCase();
apps = apps.filter(a =>
(a.name || '').toLowerCase().includes(q) ||
String(a.appid).includes(q)
);
}
// v1.9.32: 排序——按 appid 或下架时间(delistedAt/changed),空日期排末尾
apps = apps.slice().sort((a, b) => {
let r = 0;
switch (delistedState.sort) {
case 'appidAsc': r = (a.appid || 0) - (b.appid || 0); break;
case 'dateDesc': {
const sa = seriesDateToSortable(a.changed), sb = seriesDateToSortable(b.changed);
if (!sa && !sb) { r = 0; break; }
if (!sa) return 1;
if (!sb) return -1;
r = sb.localeCompare(sa); break;
}
case 'dateAsc': {
const sa = seriesDateToSortable(a.changed), sb = seriesDateToSortable(b.changed);
if (!sa && !sb) { r = 0; break; }
if (!sa) return 1;
if (!sb) return -1;
r = sa.localeCompare(sb); break;
}
case 'appidDesc':
default: r = (b.appid || 0) - (a.appid || 0);
}
return r || ((a.appid || 0) - (b.appid || 0));
});
return apps;
});
}
function appidFix(v) {
const n = Number(v);
return isNaN(n) ? v : n;
}
function getOwnedGameInfo(appid) {
// v2.3.14: 关闭"显示家庭共享"时,仅在自己拥有的游戏中查找,避免命中家庭组共享游戏
const showFamilyShared = Host.getShowFamilyShared();
const pool = showFamilyShared ? Host.getOwnedGames() : Host.getOwnedGames().filter(g => Host.isGameOwnedByMe(g));
return pool.find(g => g.appid === appidFix(appid)) || null;
}
function renderDelistedGamesList() {
const content = document.getElementById('sglv-delisted-content');
if (!content) return;
const apps = getFilteredDelistedApps();
// v2.4.0: 复用 paginate 纯函数统一分页计算
const { pageItems: pageApps, totalPages, page: _clampedPage } = Host.paginate(apps, delistedState.page, delistedState.pageSize);
delistedState.page = _clampedPage;
// v2.3.14: 使用 getActiveOwnedAppIds() 尊重"显示家庭共享"开关
const ownedIds = Host.getActiveOwnedAppIds();
if (delistedState.viewMode === 'card') {
content.innerHTML = `
${pageApps.map(app => {
const appid = appidFix(app.appid);
const isOwned = ownedIds.has(Number(appid)) || ownedIds.has(String(appid));
const typeKey = getTypeKey(app.type);
const badgeClass = isOwned ? 'owned' : 'missing';
const badgeText = isOwned ? ('✓ ' + Host.T.owned) : Host.T.missing;
const ownedInfo = isOwned ? getOwnedGameInfo(appid) : null;
const playtimeH = ownedInfo && ownedInfo.playtime > 0 ? `${(ownedInfo.playtime / 60).toFixed(1)}h` : '';
const lastPlayed = ownedInfo && ownedInfo.lastPlayed ? Host.formatLastPlayedShort(ownedInfo.lastPlayed) : '';
return `
${Host.posterImg(appid, app.name)}
${playtimeH ? `
${playtimeH}` : ''}
${app.name} ${appid}
${getTypeLabel(app.type)}
${badgeText}
${app.owners || '-'}
${app.changed ? `
${app.changed}` : ''}
${lastPlayed ? `
${lastPlayed}` : ''}
${app.achievements ? `
🏆 ${app.achievements} ` : ''}
`;
}).join('')}
`;
} else {
content.innerHTML = `
${pageApps.map(app => {
const appid = appidFix(app.appid);
const isOwned = ownedIds.has(Number(appid)) || ownedIds.has(String(appid));
const typeKey = getTypeKey(app.type);
const badgeClass = isOwned ? 'owned' : 'missing';
const badgeText = isOwned ? ('✓ ' + Host.T.owned) : Host.T.missing;
const iconUrl = Host.getGameIconUrl(appid, '');
const ownedInfo = isOwned ? getOwnedGameInfo(appid) : null;
const playtimeH = ownedInfo && ownedInfo.playtime > 0 ? `${(ownedInfo.playtime / 60).toFixed(1)}h` : '';
const lastPlayed = ownedInfo && ownedInfo.lastPlayed ? Host.formatLastPlayedShort(ownedInfo.lastPlayed) : '';
return `
${playtimeH ? `${playtimeH} ` : ''}
${badgeText}
`;
}).join('')}
`;
}
// v2.3.33:异步加载游戏中文名
content.querySelectorAll('[data-sglv-appid]').forEach(el => {
Host.loadGameZhName(el, el.getAttribute('data-sglv-appid'), el.textContent);
});
// v1.9.47-fix: 主脚本 renderPagination 所有按钮(首页/上页/下页/末页/跳页)统一走 onJump,
// 缺失第 8 参数 → 翻页按钮全部失效("只能加载第一页")。补传 onJump。
Host.renderPagination(document.getElementById('sglv-delisted-pagination'), 'sglv-delisted', delistedState.page, totalPages, apps.length,
() => { delistedState.page--; renderDelistedGamesList(); },
() => { delistedState.page++; renderDelistedGamesList(); },
(p) => { delistedState.page = p; renderDelistedGamesList(); });
}
// v1.9.47-fix: 加载骨架占位——构建完整工具栏 + 工具栏事件绑定(刷新按钮),
// 内容区域只放加载骨架条。数据到达时由 _refreshDelistedFromRemote 回调触发
// renderDelistedGamesList() 仅更新 #sglv-delisted-content,工具栏/状态保留。
// 这样既保证用户看到工具栏(可点刷新),又不会因数据未到而无法操作。
function _renderDelistedLoadingScaffold(wrap) {
// 工具栏(只放搜索框 + 视图按钮 + 刷新按钮,无数据时类型/排序/子筛选无意义,留空避免误操作)
const toolbar = document.createElement('div');
toolbar.className = 'sglv-toolbar sglv-delisted-toolbar';
toolbar.innerHTML = `
${Host.ICONS.refresh}
`;
wrap.appendChild(toolbar);
// 内容区(加载骨架)
const content = document.createElement('div');
content.className = 'sglv-content';
content.id = 'sglv-delisted-content';
const loadingText = _delistedRemoteLoading
? (Host.isZh ? '正在加载绝版游戏数据…' : 'Loading delisted game data…')
: (Host.isZh ? '暂无绝版游戏数据' : 'No delisted game data available');
content.innerHTML = '
' + loadingText + '
';
wrap.appendChild(content);
// 刷新按钮事件(复用正常路径的事件签名,只是数据为空时点击依然可走 force 全链路)
const refreshBtn = toolbar.querySelector('#sglv-delisted-refresh');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
if (refreshBtn.classList.contains('sglv-spin')) return;
refreshBtn.classList.add('sglv-spin');
const restore = () => refreshBtn.classList.remove('sglv-spin');
forceRefreshDelistedFromRemote();
setTimeout(restore, 30000);
const poll = setInterval(() => {
if (!_delistedRemoteLoading) { clearInterval(poll); restore(); }
}, 200);
});
}
}
function renderDelistedTab(parent) {
const wrap = document.createElement('div');
wrap.className = 'sglv-delisted-wrap';
// v1.9.47-fix: 首次进入触发远程源兜底预热(@resource 加载失败/同步阻塞时仍能加载)
if (!_delistedRemoteLoaded && !_delistedRemoteLoading) {
_warmDelistedRemoteCache(); // 先尝试 TTL 缓存(秒开)
if (!_delistedRemoteCache) _refreshDelistedFromRemote(false); // 无缓存 → 远程源
}
// v1.9.47-fix: 后台静默刷新节流(6h)
else if (_delistedRemoteLoaded && !_delistedRemoteLoading &&
(Date.now() - _delistedLastBgRefreshAt) > _DELISTED_REMOTE_BG_INTERVAL) {
_refreshDelistedFromRemote(false);
}
const apps = getDelistedApps();
if (apps.length === 0) {
// v1.9.47-fix: 构建完整工具栏(可点刷新) + 加载骨架占位。
// 数据到达时 _refreshDelistedFromRemote 回调 → renderDelistedGamesList()
// 仅更新 #sglv-delisted-content,工具栏/状态保留(避免重新打开 tab 闪烁)。
parent.appendChild(wrap);
_renderDelistedLoadingScaffold(wrap);
return;
}
// v2.3.14: 使用 getActiveOwnedAppIds() 尊重"显示家庭共享"开关,
// 关闭家庭共享时 KPI 与分类统计均仅计自己拥有的绝版游戏
const ownedIds = Host.getActiveOwnedAppIds();
const totalDelisted = apps.length;
let ownedDelisted = 0;
apps.forEach(app => {
if (ownedIds.has(Number(app.appid)) || ownedIds.has(String(app.appid))) ownedDelisted++;
});
const missingDelisted = totalDelisted - ownedDelisted;
const delistedPct = totalDelisted > 0 ? Math.round(ownedDelisted / totalDelisted * 100) : 0;
// KPI cards (v2.3.22: 增强样式——图标+数值+标签+子文本+彩色光晕)
const kpiSection = document.createElement('div');
kpiSection.className = 'sglv-trend-kpi-row';
kpiSection.innerHTML = `
${totalDelisted.toLocaleString()}
${Host.T.kpiDelisted}
${ownedDelisted.toLocaleString()}
${Host.T.kpiCollected}
${missingDelisted.toLocaleString()}
${Host.T.kpiToCollect}
${delistedPct}%
${Host.T.kpiCollectionProgress}
`;
wrap.appendChild(kpiSection);
// v2.3.22: 紧凑分类汇总(分页操作栏)—— 可点击徽章 + 分页
const typeCounts = {};
apps.forEach(app => {
const key = getTypeKey(app.type);
typeCounts[key] = (typeCounts[key] || 0) + 1;
});
const typeOwned = {};
apps.forEach(app => {
if (ownedIds.has(Number(app.appid)) || ownedIds.has(String(app.appid))) {
const key = getTypeKey(app.type);
typeOwned[key] = (typeOwned[key] || 0) + 1;
}
});
const typeColors = { delisted: '#f44336', purchase_disabled: '#ff9800', f2p_unavailable: '#ce93d8', retail_only: '#64b5f6', test_app: '#9e9e9e' };
const summaryBar = document.createElement('div');
summaryBar.className = 'sglv-delisted-summary-bar';
const badgesDiv = document.createElement('div');
badgesDiv.className = 'sglv-delisted-summary-badges';
const typeEntries = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
typeEntries.forEach(([key, count]) => {
const o = typeOwned[key] || 0;
const color = typeColors[key] || '#8f98a0';
const label = key === 'delisted' ? Host.T.typeDelisted
: key === 'purchase_disabled' ? Host.T.typePurchaseDisabled
: key === 'f2p_unavailable' ? Host.T.typeF2P
: key === 'retail_only' ? Host.T.typeRetailOnly
: Host.T.typeTestApp;
const badge = document.createElement('span');
badge.className = 'sglv-delisted-summary-badge';
badge.style.cssText = `background:${color}22;color:${color};border:1px solid ${color}44`;
badge.dataset.type = key;
badge.title = `${label}: ${o}/${count}`;
badge.innerHTML = `${label}
${o} / ${count} `;
badgesDiv.appendChild(badge);
});
summaryBar.appendChild(badgesDiv);
// 分页放到分类汇总行右侧
const summaryPagination = document.createElement('div');
summaryPagination.className = 'sglv-pagination sglv-delisted-pagination';
summaryPagination.id = 'sglv-delisted-pagination';
summaryBar.appendChild(summaryPagination);
wrap.appendChild(summaryBar);
// v2.3.22: 收藏进度条(置于分页操作栏下方,紧凑布局)
const progressCompact = document.createElement('div');
progressCompact.className = 'sglv-delisted-progress-compact';
progressCompact.innerHTML = `
${Host.T.kpiCollectionProgress}
${delistedPct}%
`;
wrap.appendChild(progressCompact);
// Toolbar(类型筛选 + 拥有状态筛选合并到同一行)
const toolbar = document.createElement('div');
toolbar.className = 'sglv-toolbar sglv-delisted-toolbar';
const typeButtons = [
{ key: 'all', label: Host.T.all },
{ key: 'delisted', label: Host.T.typeDelisted },
{ key: 'purchase_disabled', label: Host.T.typePurchaseDisabled },
{ key: 'f2p_unavailable', label: Host.T.typeF2P },
{ key: 'retail_only', label: Host.T.typeRetailOnly },
{ key: 'test_app', label: Host.T.typeTestApp },
];
const subItems = [
{ key: 'all', label: Host.T.delistedSubAll },
{ key: 'owned', label: Host.T.delistedSubOwned },
{ key: 'missing', label: Host.T.delistedSubMissing },
];
toolbar.innerHTML = `
${typeButtons.map(tb => `${tb.label} `).join('')}
${subItems.map(si => `${si.label} `).join('')}
${Host.isZh ? 'AppID ↓' : 'AppID ↓'}
${Host.isZh ? 'AppID ↑' : 'AppID ↑'}
${Host.isZh ? '下架时间 新→旧' : 'Delisted New→Old'}
${Host.isZh ? '下架时间 旧→新' : 'Delisted Old→New'}
${Host.ICONS.grid}
${Host.ICONS.list}
${Host.ICONS.refresh}
`;
wrap.appendChild(toolbar);
// Content area
const content = document.createElement('div');
content.className = 'sglv-content';
content.id = 'sglv-delisted-content';
wrap.appendChild(content);
parent.appendChild(wrap);
// Event bindings
// v2.3.22: 分类汇总徽章点击——快速筛选对应类型,并与工具栏按钮同步
badgesDiv.querySelectorAll('.sglv-delisted-summary-badge').forEach(badge => {
badge.addEventListener('click', () => {
delistedState.typeFilter = badge.dataset.type;
delistedState.page = 1;
toolbar.querySelectorAll('.sglv-delisted-type-btn').forEach(b => {
b.classList.toggle('active', b.dataset.type === badge.dataset.type);
});
badgesDiv.querySelectorAll('.sglv-delisted-summary-badge').forEach(b => b.style.outline = '');
badge.style.outline = `2px solid ${badge.style.color || '#66c0f4'}`;
renderDelistedGamesList();
});
});
toolbar.querySelectorAll('.sglv-delisted-sub-btn').forEach(btn => {
btn.addEventListener('click', () => {
delistedState.subFilter = btn.dataset.sub;
delistedState.page = 1;
toolbar.querySelectorAll('.sglv-delisted-sub-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderDelistedGamesList();
});
});
toolbar.querySelector('#sglv-delisted-search').addEventListener('input', _debounce((e) => {
delistedState.search = e.target.value;
delistedState.page = 1;
renderDelistedGamesList();
}, 200));
// v1.9.32: 排序下拉事件
toolbar.querySelector('#sglv-delisted-sort').addEventListener('change', (e) => {
delistedState.sort = e.target.value;
delistedState.page = 1;
renderDelistedGamesList();
});
toolbar.querySelectorAll('.sglv-delisted-type-btn').forEach(btn => {
btn.addEventListener('click', () => {
delistedState.typeFilter = btn.dataset.type;
delistedState.page = 1;
toolbar.querySelectorAll('.sglv-delisted-type-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// v2.3.22: 同步分类汇总徽章高亮
badgesDiv.querySelectorAll('.sglv-delisted-summary-badge').forEach(b => {
b.style.outline = b.dataset.type === btn.dataset.type ? `2px solid ${b.style.color || '#66c0f4'}` : '';
});
renderDelistedGamesList();
});
});
toolbar.querySelector('#sglv-delisted-card').addEventListener('click', () => {
delistedState.viewMode = 'card';
toolbar.querySelector('#sglv-delisted-card').classList.add('active');
toolbar.querySelector('#sglv-delisted-list').classList.remove('active');
renderDelistedGamesList();
});
toolbar.querySelector('#sglv-delisted-list').addEventListener('click', () => {
delistedState.viewMode = 'list';
toolbar.querySelector('#sglv-delisted-list').classList.add('active');
toolbar.querySelector('#sglv-delisted-card').classList.remove('active');
renderDelistedGamesList();
});
// v1.9.47-fix: 远程源手动刷新(@resource 加载失败时可点此走三级回退)
const refreshBtn = toolbar.querySelector('#sglv-delisted-refresh');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
if (refreshBtn.classList.contains('sglv-spin')) return;
refreshBtn.classList.add('sglv-spin');
const restore = () => refreshBtn.classList.remove('sglv-spin');
const prevLoading = _delistedRemoteLoading;
forceRefreshDelistedFromRemote();
// 监听 30s 后兜底还原(防止失败场景按钮卡在 spinning 状态)
setTimeout(restore, 30000);
// 轮询直到 loading=false 再还原
const poll = setInterval(() => {
if (!_delistedRemoteLoading) { clearInterval(poll); restore(); }
}, 200);
});
}
renderDelistedGamesList();
}
// v1.9.27: 安全读取 GM_xmlhttpRequest 响应文本
// 浏览器扩展(如 inspector.js) hook XMLHttpRequest.prototype.send 并修改 responseType 为 arraybuffer,
// 导致 r.responseText 抛 InvalidStateError。此函数先尝试 responseText,失败后尝试 response(ArrayBuffer→TextDecoder)
function _safeRespText(r) {
try {
if (r.responseText) return r.responseText;
} catch (e) { /* responseType 被外部修改为 arraybuffer */ }
try {
if (typeof r.response === 'string') return r.response;
if (r.response instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(r.response));
} catch (e2) { /* ignore */ }
return '';
}
// ==================== PS 会免标签页 (v2.6.0) ====================
// 远程数据源:psplus-games.json(由 oXnMe/psplus-steam-overlay 维护)
// 数据结构:{ updatedAt, count, games: [{ recordId, steamAppId, tier, month, monthSort, statusName, statusDisplay, statusDate, versions, psstore, color }] }
// tier: 1=Essential(必备) 2=Extra(升级) 3=Premium(高级)
// statusName: 在库/领取中/截止领取/已出库/即将出库
const PSPLUS_DATA_URL = 'https://fastly.jsdelivr.net/gh/oXnMe/psplus-steam-overlay@main/data/psplus-games.json';
// v1.9.37: 降级数据源——由 js_script/psplus-data-sync.js 每日同步镜像到 SmallRob/steam-namespace 仓库
// 主源(oXnMe)不可用时依次尝试三级 CDN 回退链(参照 EPIC_GITHUB_URLS 写法)
const PSPLUS_BACKUP_URLS = [
'https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/psplus-games.json',
'https://cdn.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/psplus-games.json',
'https://raw.githubusercontent.com/SmallRob/steam-namespace/main/data/psplus-games.json'
];
// PS Logo SVG(白色,用于占位封面)
// v2.9.22: 真实 PlayStation 标识(官方蓝紫色 P + S 经典组合,参考 PS 官方 logo SVG)
const PS_LOGO_SVG = '
';
// 从 PS Store URL 提取游戏名称(slug → Title Case)——v2.6.2 增强支持 playstation.com games slug 与 store.playstation.com product ID
function psExtractName(psstore) {
if (!psstore) return '';
// 1. playstation.com /games/{slug} 形式
const m = psstore.match(/\/games\/([^\/\?]+)/);
if (m) {
return m[1].split('-').filter(Boolean).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
}
// 2. store.playstation.com /product/{id}_{name} 形式——name 在 ID 之后可能含字母+数字
// 例如 UP1642-CUSA04798_00-SLUS211960000001 → SLUS211960000001
// JP0106-PPSA34548_00-GITAROOMAN000000 → GITAROOMAN000000
const p = psstore.match(/\/product\/[A-Z0-9\-]+_00-([A-Za-z0-9]+)/);
if (p) {
// ID 经常是大写字母+数字混合,去掉前 1-2 字母前缀尝试拆分:SLUS21196 → SLUS-21196;GITAROOMAN → GITAROO MAN
const raw = p[1];
// 尝试按"大写+小写"分词(AstroBot → Astro Bot)
const spaced = raw.replace(/([a-z])([A-Z])/g, '$1 $2');
// 尝试按"数字+字母"分词(SLUS21196 → SLUS 21196)
const spaced2 = spaced.replace(/([A-Z]+)(\d)/g, '$1 $2').replace(/(\d)([A-Z])/g, '$1 $2');
// 尝试按纯连续大写字母拆:GITAROOMAN → GITAROO MAN (按 4+ 字母)
let result = spaced2.replace(/([A-Z]{4,})/g, m => m.replace(/([A-Z]{2})([A-Z]+)/, '$1 $2').trim());
// 收尾:把数字与字母之间加空格
result = result.replace(/([A-Za-z])(\d)/g, '$1 $2').replace(/(\d)([A-Za-z])/g, '$1 $2');
return result.split(/\s+/).filter(Boolean).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
}
return '';
}
// v2.6.2: PS 独占异步抓取 psstore 页面获取中文名(基于 product ID 解析
)
// 输入 psstore URL;输出 Promise,空字符串表示获取失败
const _psStoreNamePending = new Map();
const _PS_STORE_NAME_TTL = 6 * 3600 * 1000; // 6h,与 PS 会免数据 TTL 对齐
const _PS_STORE_NAME_NEG_TTL = 30 * 60 * 1000; // 失败 30 分钟内不再请求
const SGLV_PS_NAME_KEY = 'sglvPsStoreNameCache_v1';
function sglvPsStoreNameCacheLoad() {
try { return JSON.parse(GM_getValue(SGLV_PS_NAME_KEY, '{}') || '{}'); } catch (e) { return {}; }
}
function sglvPsStoreNameCacheSave(map) {
try { GM_setValue(SGLV_PS_NAME_KEY, JSON.stringify(map || {})); } catch (e) { /* 忽略失败,不影响主流程 */ }
}
function fetchPsStoreName(psstoreUrl) {
if (!Host.isZh) return Promise.resolve('');
if (!psstoreUrl) return Promise.resolve('');
// 缓存键:URL → 名字
const cache = sglvPsStoreNameCacheLoad();
const cached = cache[psstoreUrl];
if (cached) {
if (cached.name && Date.now() - (cached.ts || 0) < _PS_STORE_NAME_TTL) return Promise.resolve(cached.name);
if (!cached.name && Date.now() - (cached.ts || 0) < _PS_STORE_NAME_NEG_TTL) return Promise.resolve('');
}
if (_psStoreNamePending.has(psstoreUrl)) return _psStoreNamePending.get(psstoreUrl);
const p = new Promise(resolve => {
const settle = (name) => {
cache[psstoreUrl] = { name, ts: Date.now() };
try { sglvPsStoreNameCacheSave(cache); } catch (e) { /* 忽略失败,不影响主流程 */ }
_psStoreNamePending.delete(psstoreUrl);
resolve(name);
};
// 只抓 zh-hans-hk 区域页面(数据源中 psstore 大多是该区域),避免美区不同步
const targetUrl = psstoreUrl.includes('playstation.com') && !psstoreUrl.includes('zh-hans-hk')
? psstoreUrl.replace(/(playstation\.com)\/([a-z\-]+\/)/, '$1/zh-hans-hk/')
: psstoreUrl;
GM_xmlhttpRequest({
method: 'GET',
url: targetUrl,
timeout: 10000,
onload(resp) {
let name = '';
try {
const html = _safeRespText(resp);
// 1. 尝试
let m = html.match(/ ]*?)property=["']og:title["']\s+(?:[^>]*?)content=["']([^"']+)/i);
if (!m) {
// 2. 尝试 ...
m = html.match(/]*>([^<]+)<\/title>/i);
}
if (!m) {
// 3. 尝试
m = html.match(/ ]*?)name=["']twitter:title["']\s+(?:[^>]*?)content=["']([^"']+)/i);
}
if (m) {
// 清理:去掉 " | PlayStation Store" 之类后缀,HTML 实体反转义
name = m[1]
.replace(/\s*\|\s*PlayStation.*$/i, '')
.replace(/\s*-\s*PlayStation.*$/i, '')
.replace(/\s*·\s*PlayStation.*$/i, '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'")
.trim();
}
} catch (e) { /* 忽略失败,不影响主流程 */ }
settle(name);
},
onerror() { settle(''); },
ontimeout() { settle(''); }
});
});
_psStoreNamePending.set(psstoreUrl, p);
return p;
}
// PS 会免占位封面 DOM(蓝色渐变 + 真实 PS 标识 + 名称),供运行时回退统一调用
function psPlaceholderDom(name) {
const holder = document.createElement('div');
holder.className = 'sglv-ps-cover-placeholder';
holder.innerHTML = '' + PS_LOGO_SVG + '
' + Host.wlEscape(name || 'PS Exclusive') + '
';
return holder;
}
// PS 会免封面图全局回退:Steam CDN 全部失败时先用 Store API 取真实封面 URL,
// API URL 仍加载失败时替换为蓝色 PS 占位(v1.1.1:修复 API URL 二次失败无兜底导致破图)
// v1.9.29: 传递 kind 参数,使 API 兜底按图片类型选择最佳 URL
unsafeWindow._sglvPsCoverFallback = function(img, appid, kind) {
if (!img || !img.parentElement) return;
Host.fetchCoverApiUrl(appid, kind).then(url => {
if (url) {
img.onerror = function() { img.onerror = null; img.parentElement.replaceChild(psPlaceholderDom(img.alt), img); };
img.src = url;
return;
}
img.parentElement.replaceChild(psPlaceholderDom(img.alt), img);
});
};
// PS 会免列表视图图标回退:CDN 图标失败时替换为 PS 蓝色占位 div
// (保持 92x43 尺寸,避免旧版 this.style.display='none' 导致整行左对齐错位)
unsafeWindow._sglvPsListIconFallback = function(img) {
if (!img || !img.parentElement) return;
const holder = document.createElement('div');
holder.className = 'sglv-list-icon';
holder.style.cssText = 'display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#003791,#0070D1);color:#fff;border-radius:4px;padding:4px';
holder.innerHTML = Host.ICONS.playstation;
img.parentElement.replaceChild(holder, img);
};
// PS 会免竖版封面:有 steamAppId → Steam 多 CDN 回退链 + PS 蓝色兜底;无 → 直接蓝色占位
// v1.9.29: ① 传递 kind 给 _sglvPsCoverFallback ② onerror 链跳过 chain[0]
// v1.9.41: good/链走 coverGoodUrl/coverChainLang
function psplusPosterImg(appid, name) {
if (!appid) {
return '' + PS_LOGO_SVG + '
' + Host.wlEscape(name || 'PS Exclusive') + '
';
}
const good = coverGoodUrl(appid, 'poster');
if (good) {
return ' ';
}
// v1.9.44: 竖版确认缺失 → 横版替代直用(交叉缓存)
const horizHtml = _sgcvHorizontalFallbackImgHtml(appid, Host.wlEscape(name), 'sglv-card-img', '_sglvPsCoverFallback');
if (horizHtml) return horizHtml;
const chain = coverChainLang(appid, 'poster');
let handler = '_sglvPsCoverFallback(this, \'' + appid + '\', \'poster\');';
for (let i = chain.length - 1; i >= 1; i--) {
handler = 'this.src=\'' + chain[i] + '\';this.onerror=function(){' + handler + '}';
}
handler = '_sgcvCoverRace(this, \'' + appid + '\', \'poster\');' + handler;
return ' ';
}
// PS 会免横版封面(capsule 大横幅)
// v1.9.29: ① 传递 kind 给 _sglvPsCoverFallback ② onerror 链跳过 chain[0]
// v1.9.41: good/链走 coverGoodUrl/coverChainLang
function psplusCapsuleImg(appid, name) {
if (!appid) {
return '' + PS_LOGO_SVG + '
' + Host.wlEscape(name || 'PS Exclusive') + '
';
}
const good = coverGoodUrl(appid, 'capsule');
if (good) {
return ' ';
}
const chain = coverChainLang(appid, 'capsule');
let handler = '_sglvPsCoverFallback(this, \'' + appid + '\', \'capsule\');';
for (let i = chain.length - 1; i >= 1; i--) {
handler = 'this.src=\'' + chain[i] + '\';this.onerror=function(){' + handler + '}';
}
handler = '_sgcvCoverRace(this, \'' + appid + '\', \'capsule\');' + handler;
return ' ';
}
// 档位标签
function psTierLabel(tier) {
if (tier === 1) return Host.T.psTierEssential;
if (tier === 2) return Host.T.psTierExtra;
if (tier === 3) return Host.T.psTierPremium;
return '?';
}
function psTierClass(tier) {
if (tier === 1) return 'tier-essential';
if (tier === 2) return 'tier-extra';
if (tier === 3) return 'tier-premium';
return '';
}
// 状态颜色映射
function psStatusColor(statusName) {
if (statusName === '在库') return '#4caf50';
if (statusName === '领取中') return '#2196f3';
if (statusName === '截止领取' || statusName === '已出库') return '#ff9800';
if (statusName === '即将出库') return '#ff5722';
return '#888';
}
// 获取游戏显示名称——v2.6.2 优先用 psstore 提取的英文名作为初始显示,中文名异步替换
// 有 steamAppId:先用 psExtractName 提取英文名(如果有 psstore),再异步 fetchGameZhName 替换为中文名
// 无 steamAppId (PS 独占):用 psExtractName 提取英文名,再异步 fetchPsStoreName 抓页面拿中文名
function psGameName(g) {
const fromPs = psExtractName(g.psstore);
if (fromPs) return fromPs;
if (g.steamAppId) return Host.isZh ? `App ${g.steamAppId}` : `App ${g.steamAppId}`;
return Host.isZh ? 'PS 独占' : 'PS Exclusive';
}
// 数据加载(@resource 兜底 + 远程 JSON 异步更新 + 永久缓存)
// v1.9.5: 缓存永不过期——优先读永久缓存秒开,后台静默全量刷新(6h 节流)
// v2.7.1: @require 库中 GM_xmlhttpRequest 可能因 Tampermonkey 沙箱隔离而静默挂起,
// 改为优先用 @resource(GM_getResourceText)同步兜底,后台异步更新缓存。
const _PSPLUS_BG_INTERVAL = 6 * 3600 * 1000; // v1.9.5: 后台刷新 6h 节流
let _psplusLastBgRefreshAt = 0;
// v1.9.24: 简单字符串哈希(djb2 变体),用于检测远程数据是否变化
function _psplusDataHash(str) {
let hash = 5381;
const len = str.length;
// 采样哈希:前500字符 + 后500字符 + 长度,避免对超大 JSON 全量遍历
const sample = str.slice(0, 500) + str.slice(Math.max(0, len - 500)) + len;
for (let i = 0; i < sample.length; i++) {
hash = ((hash << 5) + hash) + sample.charCodeAt(i);
hash = hash & hash;
}
return String(hash);
}
async function loadPsplusGames() {
// v1.9.38: 移除 force 刷新路径(刷新按钮已移除),保留缓存/资源秒开 + 后台静默刷新
if (psplusState.loading) return;
// v1.9.5: 优先读永久缓存(永不过期)
const cached = _permCacheGet('psplusGames');
if (cached && Array.isArray(cached.games) && cached.games.length > 0) {
psplusState.games = cached.games;
psplusState.updatedAt = cached.updatedAt || '';
psplusState.loaded = true;
renderPsplusSections();
// 后台静默刷新(6h 节流)
if (_shouldThrottle(_psplusLastBgRefreshAt, _PSPLUS_BG_INTERVAL)) return;
_refreshPsplusFromRemote(false);
return;
}
// 2) 永久缓存无 → 用 @resource 同步兜底(GM_getResourceText,零网络依赖)
let resourceData = null;
try {
const raw = Host.getResourceText ? Host.getResourceText('psplusData') : null;
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && Array.isArray(parsed.games) && parsed.games.length > 0) {
resourceData = parsed;
}
}
} catch (e) { /* resource 解析失败,忽略 */ }
if (resourceData) {
psplusState.games = resourceData.games;
psplusState.updatedAt = resourceData.updatedAt || '';
psplusState.loaded = true;
psplusState.loading = false;
// v1.9.26: 写入永久缓存(含 dataHash)
_permCacheSet('psplusGames', { games: resourceData.games, updatedAt: resourceData.updatedAt, dataHash: _psplusDataHash(JSON.stringify(resourceData)) });
renderPsplusSections();
// 后台异步更新(获取最新全量快照)
_refreshPsplusFromRemote(false);
return;
}
// 3) @resource 也不可用 → 回退到 GM_xmlhttpRequest 异步加载
_refreshPsplusFromRemote(true);
}
// 内部:后台异步从远程 JSON 更新 PS会免数据
// v1.9.5: 远程数据到达后全量替换(上游为每日全量快照)
// v1.9.26: 增加 _psplusDataHash 哈希对比——远程数据未变化则跳过更新/缓存写入
// v1.9.38: 移除 force 刷新(刷新按钮/spinner/状态文本已删除)
// showLoading=true 时显示 loading 状态(用于无缓存的首次加载)
function _refreshPsplusFromRemote(showLoading) {
if (typeof GM_xmlhttpRequest !== 'function') return; // 沙箱中不可用则跳过
// v1.9.38: 无手动刷新入口,loading 中直接返回避免并发请求
if (psplusState.loading) return;
// v1.9.26: 获取缓存中的数据哈希,用于远程数据变化检测
const cached = _permCacheGet('psplusGames');
const cachedHash = (cached && cached.dataHash) || '';
if (showLoading) {
psplusState.loading = true;
psplusState.loadError = null;
renderPsplusGamesList();
}
// v1.9.27: loading 安全超时——30s 后强制清除 loading(防止浏览器扩展阻断回调导致永久卡死)
let _psplusLoadingTimer = null;
if (showLoading) {
_psplusLoadingTimer = setTimeout(() => {
if (psplusState.loading) {
psplusState.loading = false;
// 尝试从永久缓存恢复数据
const cacheFallback = _permCacheGet('psplusGames');
if (cacheFallback && Array.isArray(cacheFallback.games) && cacheFallback.games.length > 0) {
psplusState.games = cacheFallback.games;
psplusState.updatedAt = cacheFallback.updatedAt || '';
psplusState.loaded = true;
psplusState.loadError = null;
} else {
psplusState.loadError = 'Timeout (loading safety)';
}
renderPsplusGamesList();
}
}, 30000);
}
// v1.9.27: 统一清理 loading 超时定时器
const _clearLoadingTimer = () => { if (_psplusLoadingTimer) { clearTimeout(_psplusLoadingTimer); _psplusLoadingTimer = null; } };
// v1.9.27: 错误时尝试从永久缓存恢复数据(避免刷新失败导致数据全部丢失)
const _restoreCacheOnError = () => {
if (psplusState.games.length === 0) {
const cacheFallback = _permCacheGet('psplusGames');
if (cacheFallback && Array.isArray(cacheFallback.games) && cacheFallback.games.length > 0) {
psplusState.games = cacheFallback.games;
psplusState.updatedAt = cacheFallback.updatedAt || '';
psplusState.loaded = true;
psplusState.loadError = null;
return true;
}
}
return false;
};
// v1.9.37: 主源 + 降级数据源多级回退——主源(oXnMe)失败时依次尝试 SmallRob/steam-namespace 镜像
const _psplusAllUrls = [PSPLUS_DATA_URL].concat(PSPLUS_BACKUP_URLS);
function _tryNextUrl(idx) {
if (idx >= _psplusAllUrls.length) {
// 所有源均失败 → 尝试从缓存恢复数据
_clearLoadingTimer();
if (showLoading) {
psplusState.loading = false;
if (_restoreCacheOnError()) {
renderPsplusSections();
} else {
psplusState.loadError = 'All sources failed';
renderPsplusGamesList();
}
}
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: _psplusAllUrls[idx],
timeout: 20000,
onload(r) {
try {
// v1.9.27: 使用 _safeRespText 防止浏览器扩展 hook XHR 导致 responseText 抛 InvalidStateError
const rawText = _safeRespText(r);
// v1.9.37: 非 2xx 响应 → 尝试下一个源
if (r.status < 200 || r.status >= 300 || !rawText) { _tryNextUrl(idx + 1); return; }
// v1.9.26: 计算远程数据哈希,与缓存哈希对比
const remoteHash = _psplusDataHash(rawText);
// v1.9.26: 哈希对比——远程数据未变化则跳过更新,保留现有缓存和状态
if (cachedHash && remoteHash === cachedHash) {
_clearLoadingTimer();
psplusState.loading = false;
_psplusLastBgRefreshAt = Date.now();
return;
}
const parsed = JSON.parse(rawText);
if (parsed && Array.isArray(parsed.games) && parsed.games.length > 0) {
_clearLoadingTimer();
// v1.9.38: 上游为全量快照,直接整体替换 state(修复 _mergeByAppid 按 appid
// 合并、但 PS会免条目无 appid 字段导致非空状态合并后数据被清空的根因)
psplusState.games = parsed.games;
psplusState.updatedAt = parsed.updatedAt || '';
psplusState.loaded = true;
psplusState.loading = false;
_psplusLastBgRefreshAt = Date.now();
// v1.9.26: 写入永久缓存(含 dataHash)
_permCacheSet('psplusGames', { games: psplusState.games, updatedAt: psplusState.updatedAt, dataHash: remoteHash });
renderPsplusSections();
return;
}
// 格式无效 → 尝试下一个源
_tryNextUrl(idx + 1);
} catch (e) {
// 解析异常 → 尝试下一个源
_tryNextUrl(idx + 1);
}
},
onerror() { _tryNextUrl(idx + 1); },
ontimeout() { _tryNextUrl(idx + 1); },
});
}
_tryNextUrl(0);
}
// 筛选 + 排序
function getFilteredPsplus() {
const q = psplusState.search.toLowerCase().trim();
const ownedIds = Host.getActiveOwnedAppIds();
const isPsOwned = g => g.steamAppId && (ownedIds.has(Number(g.steamAppId)) || ownedIds.has(String(g.steamAppId)));
let games = psplusState.games.filter(g => {
if (q) {
const name = psExtractName(g.psstore).toLowerCase();
const appid = String(g.steamAppId || '');
if (!name.includes(q) && !appid.includes(q) && !(g.month || '').includes(q)) return false;
}
if (psplusState.tierFilter !== 'all') {
// v1.6.0: catalog 档位匹配 tier 2(升级) + 3(高级)
if (psplusState.tierFilter === 'catalog') {
if (g.tier !== 2 && g.tier !== 3) return false;
} else if (String(g.tier) !== psplusState.tierFilter) return false;
}
if (psplusState.statusFilter !== 'all') {
if (psplusState.statusFilter === 'expired') {
if (g.statusName !== '截止领取' && g.statusName !== '已出库') return false;
} else if (psplusState.statusFilter === 'steamOwned') {
// v2.7.9: 直接筛选 Steam 家庭共享库已有的匹配游戏
if (!isPsOwned(g)) return false;
} else if (g.statusName !== psplusState.statusFilter) return false;
}
return true;
});
const tierOrder = { 1: 1, 2: 2, 3: 3 };
games.sort((a, b) => {
switch (psplusState.sort) {
case 'dateAsc': return (a.monthSort || '').localeCompare(b.monthSort || '');
case 'tierAsc': return (tierOrder[a.tier] || 9) - (tierOrder[b.tier] || 9) || (b.monthSort || '').localeCompare(a.monthSort || '');
case 'tierDesc': return (tierOrder[b.tier] || 9) - (tierOrder[a.tier] || 9) || (b.monthSort || '').localeCompare(a.monthSort || '');
case 'nameAsc': return _nameCollator.compare(psExtractName(a.psstore) || '~', psExtractName(b.psstore) || '~');
case 'dateDesc':
default: return (b.monthSort || '').localeCompare(a.monthSort || '');
}
});
return games;
}
// ----- 渲染 -----
function renderPsplusTab(parent) {
const wrap = document.createElement('div');
wrap.className = 'sglv-psplus-wrap';
// KPI 区域
const kpi = document.createElement('div');
kpi.id = 'sglv-psplus-kpi';
wrap.appendChild(kpi);
// 工具栏(v2.6.1:紧凑两行——第1行 搜索+排序+视图;第2行 筛选按钮+分页)
const toolbar = document.createElement('div');
toolbar.className = 'sglv-toolbar';
const sortOptions = [
{ key: 'dateDesc', label: Host.T.psSortDateDesc },
{ key: 'dateAsc', label: Host.T.psSortDateAsc },
{ key: 'tierAsc', label: Host.T.psSortTierAsc },
{ key: 'tierDesc', label: Host.T.psSortTierDesc },
{ key: 'nameAsc', label: Host.T.psSortNameAsc },
];
toolbar.innerHTML = `
${sortOptions.map(o => `${o.label} `).join('')}
${Host.ICONS.grid}
${Host.ICONS.cover}
${Host.ICONS.list}
`;
wrap.appendChild(toolbar);
// 筛选栏(档位 + 状态)——分页也放这一行(v2.6.1 紧凑操作栏)
const filterBar = document.createElement('div');
filterBar.className = 'sglv-filter-bar';
const tierBtns = [
{ key: 'all', label: Host.T.all },
{ key: '1', label: Host.T.psTierEssential },
{ key: '2', label: Host.T.psTierExtra },
{ key: '3', label: Host.T.psTierPremium },
];
const statusBtns = [
{ key: 'all', label: Host.T.psStatusAll },
{ key: '在库', label: Host.T.psStatusIn },
{ key: '领取中', label: Host.T.psStatusActive },
{ key: 'expired', label: Host.T.psStatusExpired },
{ key: '即将出库', label: Host.T.psStatusLeaving },
{ key: 'steamOwned', label: Host.T.steamInLib },
];
filterBar.innerHTML = `
${tierBtns.map(b => `${b.label} `).join('')}
${statusBtns.map(b => `${b.label} `).join('')}
`;
wrap.appendChild(filterBar);
// 游戏列表容器
const content = document.createElement('div');
content.className = 'sglv-content';
content.id = 'sglv-psplus-content';
wrap.appendChild(content);
parent.appendChild(wrap);
// 事件绑定
toolbar.querySelector('#sglv-psplus-search').addEventListener('input', _debounce((e) => {
psplusState.search = e.target.value;
psplusState.page = 1;
renderPsplusGamesList();
}, 200));
toolbar.querySelector('#sglv-psplus-sort').addEventListener('change', (e) => {
psplusState.sort = e.target.value;
psplusState.page = 1;
renderPsplusGamesList();
});
const setPsViewMode = (mode) => {
psplusState.viewMode = mode;
['card', 'cover', 'list'].forEach(m => {
toolbar.querySelector('#sglv-ps-' + m)?.classList.toggle('active', m === mode);
});
renderPsplusGamesList();
};
toolbar.querySelector('#sglv-ps-card').addEventListener('click', () => setPsViewMode('card'));
toolbar.querySelector('#sglv-ps-cover').addEventListener('click', () => setPsViewMode('cover'));
toolbar.querySelector('#sglv-ps-list').addEventListener('click', () => setPsViewMode('list'));
filterBar.querySelectorAll('.sglv-ps-tier-btn').forEach(btn => {
btn.addEventListener('click', () => {
psplusState.tierFilter = btn.dataset.tier;
psplusState.page = 1;
filterBar.querySelectorAll('.sglv-ps-tier-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderPsplusKpi(); // v1.6.0: 同步 KPI active 高亮
renderPsplusGamesList();
});
});
filterBar.querySelectorAll('.sglv-ps-status-btn').forEach(btn => {
btn.addEventListener('click', () => {
psplusState.statusFilter = btn.dataset.status;
psplusState.page = 1;
filterBar.querySelectorAll('.sglv-ps-status-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderPsplusKpi(); // v1.6.0: 同步 KPI active 高亮
renderPsplusGamesList();
});
});
// v1.6.0: 筛选改为点击 KPI 卡片切换(再次点击同一卡片回到「全部」),与年度大作/Epic赠送标签页一致
kpi.addEventListener('click', e => {
const card = e.target.closest('[data-psplus-filter]');
if (!card) return;
const f = card.dataset.psplusFilter;
const activeKpi = getPsplusActiveKpi();
if (activeKpi === f) {
// 再次点击同一卡片 → 回到「全部」
psplusState.tierFilter = 'all';
psplusState.statusFilter = 'all';
} else {
switch (f) {
case 'all': psplusState.tierFilter = 'all'; psplusState.statusFilter = 'all'; break;
case 'essential': psplusState.tierFilter = '1'; psplusState.statusFilter = 'all'; break;
case 'catalog': psplusState.tierFilter = 'catalog'; psplusState.statusFilter = 'all'; break;
case 'steamOwned': psplusState.tierFilter = 'all'; psplusState.statusFilter = 'steamOwned'; break;
}
}
psplusState.page = 1;
syncPsplusFilterBtns(filterBar); // 同步筛选栏按钮 active 状态
renderPsplusKpi(); // 刷新 KPI active 高亮
renderPsplusGamesList();
});
if (!psplusState.loaded && !psplusState.loading) {
loadPsplusGames();
} else {
renderPsplusSections();
}
}
function renderPsplusSections() {
if (_collectionActiveTab !== 'psplus' || !document.getElementById('sglv-psplus-content')) return;
renderPsplusKpi();
renderPsplusGamesList();
renderPsplusRefreshStatus();
}
/** v1.9.38: 渲染 PS会免工具栏状态文本(仅显示更新时间,手动刷新已移除) */
function renderPsplusRefreshStatus() {
const el = document.getElementById('sglv-ps-status');
if (!el) return;
const zh = Host.isZh;
const date = psplusState.updatedAt ? psplusState.updatedAt.slice(0, 10) : '';
el.textContent = date ? (zh ? '更新: ' : 'Updated: ') + date : '';
el.className = 'sglv-ps-status';
}
// v1.6.0: 根据 tierFilter + statusFilter 推断当前激活的 KPI 卡片
function getPsplusActiveKpi() {
if (psplusState.tierFilter === 'all' && psplusState.statusFilter === 'all') return 'all';
if (psplusState.tierFilter === '1' && psplusState.statusFilter === 'all') return 'essential';
if (psplusState.tierFilter === 'catalog' && psplusState.statusFilter === 'all') return 'catalog';
if (psplusState.tierFilter === 'all' && psplusState.statusFilter === 'steamOwned') return 'steamOwned';
return ''; // 自定义组合,无 KPI 高亮
}
// v1.6.0: KPI 点击后同步筛选栏按钮 active 状态
function syncPsplusFilterBtns(filterBar) {
filterBar.querySelectorAll('.sglv-ps-tier-btn').forEach(b => {
b.classList.toggle('active', b.dataset.tier === psplusState.tierFilter);
});
filterBar.querySelectorAll('.sglv-ps-status-btn').forEach(b => {
b.classList.toggle('active', b.dataset.status === psplusState.statusFilter);
});
}
function renderPsplusKpi() {
const el = document.getElementById('sglv-psplus-kpi');
if (!el) return;
if (!psplusState.loaded) { el.innerHTML = ''; return; }
const games = psplusState.games;
const total = games.length;
const essential = games.filter(g => g.tier === 1).length;
const catalog = games.filter(g => g.tier === 2 || g.tier === 3).length;
const steamLinked = games.filter(g => g.steamAppId).length;
const ownedIds = Host.getActiveOwnedAppIds();
const steamOwned = games.filter(g => g.steamAppId && (ownedIds.has(Number(g.steamAppId)) || ownedIds.has(String(g.steamAppId)))).length;
const updatedStr = psplusState.updatedAt ? (Host.isZh ? '更新于 ' : 'Updated ') + psplusState.updatedAt.slice(0, 10) : '';
const activeKpi = getPsplusActiveKpi();
const tipAll = Host.isZh ? '点击筛选:全部' : 'Click to filter: All';
const tipEssential = Host.isZh ? '点击筛选:必备档' : 'Click to filter: Essential';
const tipCatalog = Host.isZh ? '点击筛选:升级/高级档' : 'Click to filter: Extra/Premium';
const tipSteam = Host.isZh ? '点击筛选:Steam 已拥有' : 'Click to filter: Owned on Steam';
el.innerHTML = `
${total.toLocaleString()}
${updatedStr}
${essential.toLocaleString()}
${Host.T.psTierEssential}
${catalog.toLocaleString()}
${Host.T.psTierExtra} / ${Host.T.psTierPremium}
${steamOwned.toLocaleString()}
${Host.T.psKpiSteamTotal.replace('{n}', steamLinked)}
`;
}
function renderPsplusGamesList() {
const content = document.getElementById('sglv-psplus-content');
if (!content) return;
const clearPagination = () => { const p = document.getElementById('sglv-psplus-pagination'); if (p) p.innerHTML = ''; };
if (psplusState.loading) {
content.innerHTML = '' + Host.T.psLoading + '
';
clearPagination();
return;
}
if (psplusState.loadError) {
content.innerHTML = '' + Host.T.psLoadError + '
';
clearPagination();
return;
}
if (!psplusState.loaded || psplusState.games.length === 0) {
content.innerHTML = '' + Host.T.psLoading + '
';
clearPagination();
return;
}
const games = getFilteredPsplus();
if (games.length === 0) {
content.innerHTML = '' + (Host.isZh ? '无匹配结果' : 'No matching results') + '
';
clearPagination();
return;
}
const { pageItems, totalPages, page: _clampedPage } = Host.paginate(games, psplusState.page, psplusState.pageSize);
psplusState.page = _clampedPage;
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = g => g.steamAppId && (ownedIds.has(Number(g.steamAppId)) || ownedIds.has(String(g.steamAppId)));
const tierBadgeHtml = g => '' + psTierLabel(g.tier) + ' ';
const statusBadgeHtml = g => '' + Host.wlEscape(g.statusDisplay || g.statusName) + ' ';
const versionsHtml = g => (g.versions || []).map(v => '' + v + ' ').join(' ');
const ownershipBadgeHtml = g => isOwned(g) ? '✓ ' + Host.T.steamInLib + ' ' : '';
// 卡片视图
if (psplusState.viewMode === 'card') {
content.innerHTML = '' + pageItems.map(g => {
const appid = Number(g.steamAppId) || 0;
const name = psGameName(g);
const clickUrl = appid ? 'https://store.steampowered.com/app/' + appid : (g.psstore || '#');
// v2.6.2: 名称 span 加 data-sglv-psstore 以便异步替换 PS 独占中文名
const nameSpan = appid
? '
' + Host.wlEscape(name) + ' ' + appid + ' '
: '
' + Host.wlEscape(name) + ' ';
return '
' +
'
' + tierBadgeHtml(g) + psplusPosterImg(appid, name) + '
' +
'
' +
'
' + nameSpan + '
' +
'
' + statusBadgeHtml(g) + ownershipBadgeHtml(g) + '
' +
'
' + Host.ICONS.clock + ' ' + Host.wlEscape(g.month || '') + ' ' + versionsHtml(g) + '
' +
'
' +
'
';
}).join('') + '
';
} else if (psplusState.viewMode === 'cover') {
// 封面视图(横板大图)
// v2.9.22: 信息行紧凑到 3 行:标题 / [状态+在库] / [月份+版本]
content.innerHTML = '' + pageItems.map(g => {
const appid = Number(g.steamAppId) || 0;
const name = psGameName(g);
const clickUrl = appid ? 'https://store.steampowered.com/app/' + appid : (g.psstore || '#');
const nameSpan = appid
? '
' + Host.wlEscape(name) + ' ' + appid + ' '
: '
' + Host.wlEscape(name) + ' ';
return '
' +
'
' + tierBadgeHtml(g) + psplusCapsuleImg(appid, name) + '
' +
'
' +
'
' + nameSpan + '
' +
'
' + statusBadgeHtml(g) + ownershipBadgeHtml(g) + '
' +
'
' + Host.ICONS.clock + ' ' + Host.wlEscape(g.month || '') + ' ' + versionsHtml(g) + '
' +
'
' +
'
';
}).join('') + '
';
} else {
// 列表视图
content.innerHTML = '' + pageItems.map(g => {
const appid = Number(g.steamAppId) || 0;
const name = psGameName(g);
const clickUrl = appid ? 'https://store.steampowered.com/app/' + appid : (g.psstore || '#');
const iconHtml = appid
? '
'
: '
' + Host.ICONS.playstation + '
';
const nameSpan = appid
? '
' + Host.wlEscape(name) + ' ' + appid + ' '
: '
' + Host.wlEscape(name) + ' ';
return '
' +
iconHtml +
'
' +
'
' + nameSpan + '
' +
'
' + tierBadgeHtml(g) + statusBadgeHtml(g) + ownershipBadgeHtml(g) + '' + Host.wlEscape(g.month || '') + ' ' + versionsHtml(g) + '
' +
'
' +
'
';
}).join('') + '
';
}
// v1.9.47-fix: 补 onJump——PS会免翻页 5 个按钮全部走 onJump(参见主脚本 renderPagination)
const pagination = document.getElementById('sglv-psplus-pagination');
if (pagination) {
Host.renderPagination(pagination, 'sglv-psplus', psplusState.page, totalPages, games.length,
() => { psplusState.page--; renderPsplusGamesList(); },
() => { psplusState.page++; renderPsplusGamesList(); },
(p) => { psplusState.page = p; renderPsplusGamesList(); });
}
// v2.6.2: 异步加载中文名——同时支持 steam appid 路径与 PS 独占 psstore 路径
content.querySelectorAll('[data-sglv-appid]').forEach(el => {
Host.loadGameZhName(el, el.getAttribute('data-sglv-appid'), el.textContent);
});
// v2.6.2: PS 独占(无 steamAppId)——异步抓 psstore 页面替换为中文名
content.querySelectorAll('[data-sglv-psstore]').forEach(el => {
const psUrl = el.getAttribute('data-sglv-psstore');
if (!psUrl) return;
// 已经有 appid 的由 loadGameZhName 处理,这里跳过
if (el.hasAttribute('data-sglv-appid')) return;
const original = el.textContent;
fetchPsStoreName(psUrl).then(zhName => {
if (zhName && zhName !== original) {
el.textContent = zhName;
el.title = `${zhName} (${original})`;
}
});
});
}
// ==================== Epic 赠送标签页 (v1.1.0) ====================
// 数据来源:GitHub 自建数据源 epic_freebies.json(epic-freebies-weekly-sync.js 周同步生产,jsdelivr 三级回退)。
// v1.9.19: 已移除 keylol 抓取兜底(帖子需登录阅读权限,无法获取)。
// 与 "E宝的爱标识助手" 脚本共享 GM 存储 key 'epic_freebie_ids',可互相补充。
// 数据仅为 Steam appid 集合(无名称/日期/档位),名称经 Steam 中文名异步加载。
// 多级兜底:① IDB 永久缓存 → ② GM 存储 epic_freebie_ids + 内置名单 → ③ 远程抓取(GitHub 数据源三级回退)→ ④ 手动 JSON 导入。
// 拥有判定:getActiveOwnedAppIds()(尊重家庭组开关),匹配当前 Steam 用户家庭共享游戏库。
// v1.9.17: GitHub 自建数据源三级回退链(参照年度大作 GOTY_DATA_SOURCES 写法)——
// 优先 jsdelivr(国内稳定),raw.githubusercontent 兜底;v1.9.19 起全部失败即视为刷新失败。
const EPIC_GITHUB_URLS = [
'https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/epic_freebies.json', // ① fastly 节点(优先)
'https://cdn.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/epic_freebies.json', // ② jsdelivr 主节点
'https://raw.githubusercontent.com/SmallRob/steam-namespace/main/data/epic_freebies.json' // ③ raw 兜底
];
const EPIC_GM_KEY = 'epic_freebie_ids'; // 与 E宝的爱标识助手 共享
// 内置兜底名单(取自 E宝的爱标识助手 MANUAL_FREEBIE_IDS)
const EPIC_MANUAL_IDS = ['690640','1564220','822094','750920','391220','203160','362960','346940','234650','300550','49520','261640'];
// v2.9.22: Epic Games 盾形标识(深色盾形 + 白色 EPIC/GAMES 文字,参考 Epic 官方 logo)
const EPIC_LOGO_SVG = 'EPIC GAMES ';
// 名称缓存(会话级,供搜索/排序使用;中文复用宿主持久化缓存)
const _epicNameCache = new Map();
const _epicNamePending = new Map();
// v1.1.1: 通过 appdetails 获取英文名(filters=basic 含 name 字段;参考 Steam_Show_English_Name.js)
// 旧版 filters=name 是无效过滤值,appdetails 返回 data:[],英文名始终为空。
function _epicFetchEnName(id) {
return new Promise(resolve => {
if (typeof GM_xmlhttpRequest !== 'function') { resolve(''); return; }
GM_xmlhttpRequest({
method: 'GET',
url: 'https://store.steampowered.com/api/appdetails?appids=' + id + '&filters=basic&l=english',
timeout: 10000,
onload(r) {
let name = '';
try { const j = JSON.parse(_safeRespText(r)); const d = j && j[id]; if (d && d.success && d.data && d.data.name) name = d.data.name; } catch (e) { /* 忽略失败,不影响主流程 */ }
resolve(name);
},
onerror() { resolve(''); },
ontimeout() { resolve(''); }
});
});
}
function epicLoadName(appid) {
const id = Number(appid);
if (!id) return Promise.resolve('');
if (_epicNameCache.has(id)) return Promise.resolve(_epicNameCache.get(id));
if (_epicNamePending.has(id)) return _epicNamePending.get(id);
// 默认中文名:中文环境优先复用宿主持久化中文名缓存(含 30 天 TTL + 负缓存);
// 中文名获取失败(游戏无中文名 / 请求失败)→ 回退英文名,确保至少展示一种语言名称。
const p = (async () => {
if (Host.isZh && typeof Host.fetchGameZhName === 'function') {
const zh = await Host.fetchGameZhName(id);
if (zh) { _epicNameCache.set(id, zh); _epicNamePending.delete(id); return zh; }
}
const en = await _epicFetchEnName(id);
_epicNameCache.set(id, en || '');
_epicNamePending.delete(id);
return en || '';
})();
_epicNamePending.set(id, p);
return p;
}
function epicDisplayName(appid) {
const cached = _epicNameCache.get(Number(appid));
if (cached) return cached;
return 'App ' + appid;
}
function epicSourceLabel(source) {
if (source === 'github') return Host.T.epicSourceGithub || (Host.isZh ? 'GitHub 数据源' : 'GitHub');
if (source === 'keylol') return Host.T.epicSourceKeylol;
if (source === 'local') return Host.T.epicSourceLocal;
if (source === 'import') return Host.T.epicSourceImport;
return Host.T.epicSourceManual;
}
// Epic 占位封面(HTML 字符串,用于无 appid 时初始渲染)——深色盾形 + EPIC 标识 + 名称
function epicPlaceholder(name) {
return '' + EPIC_LOGO_SVG + '
' + Host.wlEscape(name || 'Epic') + '
';
}
// Epic 占位封面 DOM(深色渐变 + 真实 Epic 盾形标识 + 名称),供运行时回退统一调用
function epicPlaceholderDom(name) {
const holder = document.createElement('div');
holder.className = 'sglv-ps-cover-placeholder sglv-epic-cover-placeholder';
holder.style.background = 'linear-gradient(135deg,#0d0d0d,#1f1f1f)';
holder.innerHTML = '' + EPIC_LOGO_SVG + '
' + Host.wlEscape(name || 'Epic') + '
';
return holder;
}
// Epic 封面全局回退:Steam CDN 全部失败时先用 Store API 取真实封面 URL,
// API URL 仍加载失败时替换为深色 Epic 占位(v1.1.1:修复 API URL 二次失败无兜底导致破图)
// v1.9.29: 传递 kind 参数,使 API 兜底按图片类型选择最佳 URL
unsafeWindow._sglvEpicCoverFallback = function(img, appid, kind) {
if (!img || !img.parentElement) return;
Host.fetchCoverApiUrl(appid, kind).then(url => {
if (url) {
img.onerror = function() { img.onerror = null; img.parentElement.replaceChild(epicPlaceholderDom(img.alt), img); };
img.src = url;
return;
}
img.parentElement.replaceChild(epicPlaceholderDom(img.alt), img);
});
};
// 竖版封面(卡片视图)
// v1.9.29: ① 传递 kind 给 _sglvEpicCoverFallback ② onerror 链跳过 chain[0]
// v1.9.41: good/链走 coverGoodUrl/coverChainLang
function epicPosterImg(appid, name) {
if (!appid) return epicPlaceholder(name);
const good = coverGoodUrl(appid, 'poster');
if (good) {
return ' ';
}
// v1.9.44: 竖版确认缺失 → 横版替代直用(交叉缓存)
const horizHtml = _sgcvHorizontalFallbackImgHtml(appid, Host.wlEscape(name), 'sglv-card-img', '_sglvEpicCoverFallback');
if (horizHtml) return horizHtml;
const chain = coverChainLang(appid, 'poster');
let handler = '_sglvEpicCoverFallback(this, \'' + appid + '\', \'poster\');';
for (let i = chain.length - 1; i >= 1; i--) {
handler = 'this.src=\'' + chain[i] + '\';this.onerror=function(){' + handler + '}';
}
handler = '_sgcvCoverRace(this, \'' + appid + '\', \'poster\');' + handler;
return ' ';
}
// 横版封面(封面视图大横幅)
// v1.9.29: ① 传递 kind 给 _sglvEpicCoverFallback ② onerror 链跳过 chain[0]
// v1.9.41: good/链走 coverGoodUrl/coverChainLang
function epicCapsuleImg(appid, name) {
if (!appid) return epicPlaceholder(name);
const good = coverGoodUrl(appid, 'capsule');
if (good) {
return ' ';
}
const chain = coverChainLang(appid, 'capsule');
let handler = '_sglvEpicCoverFallback(this, \'' + appid + '\', \'capsule\');';
for (let i = chain.length - 1; i >= 1; i--) {
handler = 'this.src=\'' + chain[i] + '\';this.onerror=function(){' + handler + '}';
}
handler = '_sgcvCoverRace(this, \'' + appid + '\', \'capsule\');' + handler;
return ' ';
}
// 徽章
function epicFreeBadgeHtml() {
return '' + Host.T.epicBadgeFree + ' ';
}
function epicOwnedBadgeHtml(owned) {
return owned
? '✓ ' + Host.T.steamInLib + ' '
: '' + Host.T.epicBadgeNotOwned + ' ';
}
// 合并多个来源 → 去重数字数组
function mergeEpicIds(...sources) {
const set = new Set();
for (const src of sources) {
if (!Array.isArray(src)) continue;
for (const id of src) {
const n = Number(id);
if (n > 0) set.add(n);
}
}
return Array.from(set);
}
// 加载 Epic 赠送数据(多级兜底)
// v1.9.5: 缓存永不过期——优先读永久缓存秒开,后台增量合并刷新(6h 节流)
const _EPIC_BG_INTERVAL = 6 * 3600 * 1000;
let _epicLastBgRefreshAt = 0;
function loadEpicGames(force = false) {
// v1.9.27: force 时重置可能卡死的 loading 标志
if (epicState.loading) {
if (!force) return;
epicState.loading = false;
}
// v1.9.5: 优先读永久缓存(永不过期)
if (!force) {
const cached = _permCacheGet('epicFreeGames');
if (cached && Array.isArray(cached.appids) && cached.appids.length > 0) {
epicState.appids = cached.appids;
epicState.updatedAt = cached.updatedAt || '';
epicState.source = cached.source || '';
epicState.loaded = true;
renderEpicSections();
// 后台增量刷新(6h 节流)
if (_shouldThrottle(_epicLastBgRefreshAt, _EPIC_BG_INTERVAL)) return;
_refreshEpicFromRemote(false, false);
return;
}
}
// 2) GM 存储 + 内置名单(即时兜底)
let gmIds = [];
try { gmIds = GM_getValue(EPIC_GM_KEY, []) || []; } catch (e) { gmIds = []; }
const merged = mergeEpicIds(gmIds, EPIC_MANUAL_IDS);
if (merged.length > 0) {
epicState.appids = merged;
epicState.updatedAt = '';
epicState.source = gmIds.length > 0 ? 'local' : 'manual';
epicState.loaded = true;
// v1.9.5: 写入永久缓存
_permCacheSet('epicFreeGames', { appids: merged, updatedAt: '', source: epicState.source });
if (!force) renderEpicSections();
}
// 3) 远程抓取(GitHub 数据源三级回退;force 时显示 loading,失败回退到本地数据)
_refreshEpicFromRemote(force, force);
}
// 远程抓取失败时的统一处理(有本地数据则保留并提示,无数据则显示错误)
function _epicFailLoading(msg) {
epicState.loading = false;
if (epicState.appids.length === 0) {
epicState.loadError = msg;
} else {
epicState.loadError = null;
Host.showToast(Host.isZh ? '更新失败,已使用本地数据' : 'Update failed, using local data');
}
renderEpicGamesList();
}
// 内部:远程刷新 Epic 数据(v1.9.17 改造,v1.9.19 移除 keylol 兜底)——
// 仅走 GitHub 数据源三级回退链(fastly.jsdelivr → cdn.jsdelivr → raw.githubusercontent,
// 参照年度大作 _refreshGotyFromRemote)。每个请求均带 timeout,
// onerror/ontimeout 一律降级到下一个 URL,绝不挂起
// (参照 v2.7.1 沙箱挂起历史教训:任何分支都必须走到 settle)。
function _refreshEpicFromRemote(force, showLoading) {
if (typeof GM_xmlhttpRequest !== 'function') {
if (showLoading) _epicFailLoading('noXhr');
return;
}
// v1.9.27: force 时重置可能卡死的 loading 标志
if (epicState.loading) {
if (!force) return;
epicState.loading = false;
}
if (showLoading) { epicState.loading = true; epicState.loadError = null; renderEpicGamesList(); }
// 兼容两种 JSON 格式:数组 appid 列表 / 对象 { _meta?, updatedAt?, appids: number[], games?: [...] }
function _extractEpicAppids(data) {
if (Array.isArray(data)) return data;
if (data && typeof data === 'object') {
if (Array.isArray(data.appids)) return data.appids;
if (Array.isArray(data.games)) return data.games.map(g => (g && g.appid)).filter(Boolean);
}
return null;
}
// 成功响应公共逻辑:提取 appid → mergeEpicIds 增量合并(只增不减)→ 写缓存 → 渲染
function _processEpicJson(data) {
const incoming = mergeEpicIds(_extractEpicAppids(data));
if (incoming.length === 0) return false;
let gmIds = [];
try { gmIds = GM_getValue(EPIC_GM_KEY, []) || []; } catch (e) { /* 忽略失败,不影响主流程 */ }
// v1.9.18: 并入当前缓存历史(只增不减),远程数据变小也不丢本地历史 appid
const merged = mergeEpicIds(incoming, epicState.appids, gmIds, EPIC_MANUAL_IDS);
epicState.appids = merged;
epicState.updatedAt = (data && typeof data === 'object' && data.updatedAt) ? String(data.updatedAt).slice(0, 10) : new Date().toISOString().slice(0, 10);
epicState.source = 'github';
epicState.loaded = true;
epicState.loading = false;
epicState.loadError = null;
_epicLastBgRefreshAt = Date.now(); // v1.9.5: 记录后台刷新时间戳
try { GM_setValue(EPIC_GM_KEY, incoming); } catch (e) { /* 忽略失败,不影响主流程 */ }
// v1.9.5: 写入永久缓存(永不过期)+ TTL 缓存(兼容)
_permCacheSet('epicFreeGames', { appids: merged, updatedAt: epicState.updatedAt, source: 'github' });
try { Host.cacheSet('epicFreeGames', { appids: merged, updatedAt: epicState.updatedAt, source: 'github' }, Host.cacheTTL.epic); } catch (e) { /* 忽略失败 */ }
renderEpicSections();
return true;
}
// 依次尝试 EPIC_GITHUB_URLS,任一成功即停止;全部失败 → 视为刷新失败(保留本地缓存数据)
function _tryNextGithubSource(idx) {
if (idx >= EPIC_GITHUB_URLS.length) {
// v1.9.19: keylol 兜底已移除(帖子需登录阅读权限)——force 刷新提示失败并用本地数据,后台刷新静默
if (showLoading) _epicFailLoading('network');
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: EPIC_GITHUB_URLS[idx],
timeout: 15000,
onload(r) {
try {
if (r.status !== 404 && _safeRespText(r)) {
if (_processEpicJson(JSON.parse(_safeRespText(r)))) return; // 成功,停止
}
} catch (e) { /* 解析异常,交给下一个源 */ }
_tryNextGithubSource(idx + 1); // 失败,尝试下一个源
},
onerror() { _tryNextGithubSource(idx + 1); },
ontimeout() { _tryNextGithubSource(idx + 1); },
});
}
_tryNextGithubSource(0);
}
// 手动 JSON 导入(兜底:远程抓取失败时)
function importEpicJson() {
const raw = prompt(Host.T.epicImportPrompt, '');
if (!raw || !raw.trim()) return;
try {
const data = JSON.parse(raw);
let ids = [];
if (Array.isArray(data)) ids = data;
else if (Array.isArray(data.appids)) ids = data.appids;
else if (Array.isArray(data.games)) ids = data.games.map(g => g.appid || g.steamAppId || g.id).filter(Boolean);
const incoming = mergeEpicIds(ids);
if (incoming.length === 0) { Host.showToast(Host.T.epicImportNoData); return; }
let gmIds = [];
try { gmIds = GM_getValue(EPIC_GM_KEY, []) || []; } catch (e) { /* 忽略失败,不影响主流程 */ }
const merged = mergeEpicIds(incoming, gmIds, EPIC_MANUAL_IDS);
try { GM_setValue(EPIC_GM_KEY, incoming); } catch (e) { /* 忽略失败,不影响主流程 */ }
try { Host.cacheSet('epicFreeGames', { appids: merged, updatedAt: new Date().toISOString().slice(0, 10), source: 'import' }, Host.cacheTTL.epic); } catch (e) { /* 忽略失败,不影响主流程 */ }
epicState.appids = merged;
epicState.updatedAt = new Date().toISOString().slice(0, 10);
epicState.source = 'import';
epicState.loaded = true;
epicState.loading = false;
epicState.loadError = null;
epicState.page = 1;
Host.showToast(Host.T.epicImportOk.replace('{n}', merged.length));
renderEpicSections();
} catch (e) {
Host.showToast(Host.T.epicImportFail + e.message);
}
}
// v1.9.18: 一键导出全量 JSON(含缓存历史)——epic_freebies.json 兼容格式,
// 可经 js_script/epic-freebies-weekly-sync.js --import 回流 GitHub 数据源维护全量名单
function exportEpicJson() {
const ids = mergeEpicIds(epicState.appids).sort((a, b) => a - b);
if (ids.length === 0) {
Host.showToast(Host.T.epicExportEmpty || (Host.isZh ? '没有可导出的 Epic 赠送数据' : 'No Epic data to export'));
return;
}
const payload = {
_meta: { source: 'sglv-epic-export', version: 1 },
updatedAt: new Date().toISOString(),
appids: ids
};
const text = JSON.stringify(payload, null, 2);
const filename = 'epic_freebies_export_' + new Date().toISOString().slice(0, 10) + '.json';
try {
const blob = new Blob([text], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 5000);
Host.showToast((Host.T.epicExportOk || (Host.isZh ? '已导出 {n} 款 Epic 赠送游戏' : 'Exported {n} Epic free games')).replace('{n}', String(ids.length)));
} catch (e) {
// 兜底:Blob 下载不可用时弹窗展示 JSON 供手工复制保存
prompt(Host.T.epicExportFail || (Host.isZh ? '自动下载失败,请手工复制以下 JSON:' : 'Auto download failed. Copy JSON below:'), text);
}
}
// 筛选 + 排序
function getFilteredEpic() {
const q = epicState.search.toLowerCase().trim();
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
let list = epicState.appids.map(Number).filter(n => n > 0);
if (q) {
list = list.filter(n => {
if (String(n).includes(q)) return true;
const name = (_epicNameCache.get(n) || '').toLowerCase();
return name.includes(q);
});
}
if (epicState.ownFilter === 'owned') list = list.filter(isOwned);
else if (epicState.ownFilter === 'notOwned') list = list.filter(n => !isOwned(n));
list.sort((a, b) => {
switch (epicState.sort) {
case 'appidAsc': return a - b;
case 'nameAsc': return _nameCollator.compare(_epicNameCache.get(a) || '~', _epicNameCache.get(b) || '~') || (a - b);
case 'appidDesc':
default: return b - a;
}
});
return list;
}
// ----- 渲染 -----
function renderEpicTab(parent) {
const wrap = document.createElement('div');
wrap.className = 'sglv-psplus-wrap';
// KPI 区域
const kpi = document.createElement('div');
kpi.id = 'sglv-epic-kpi';
wrap.appendChild(kpi);
// 工具栏:搜索 + 排序 + 视图 + 导入 + 刷新
const toolbar = document.createElement('div');
toolbar.className = 'sglv-toolbar';
const sortOptions = [
{ key: 'appidDesc', label: Host.T.epicSortAppidDesc },
{ key: 'appidAsc', label: Host.T.epicSortAppidAsc },
{ key: 'nameAsc', label: Host.T.epicSortNameAsc },
];
toolbar.innerHTML = `
${sortOptions.map(o => `${o.label} `).join('')}
${Host.ICONS.grid}
${Host.ICONS.cover}
${Host.ICONS.list}
${Host.ICONS.tag}
${Host.ICONS.download || ' '}
${Host.ICONS.refresh}
`;
wrap.appendChild(toolbar);
// 游戏列表容器
const content = document.createElement('div');
content.className = 'sglv-content';
content.id = 'sglv-epic-content';
wrap.appendChild(content);
parent.appendChild(wrap);
// 事件绑定
toolbar.querySelector('#sglv-epic-search').addEventListener('input', _debounce((e) => {
epicState.search = e.target.value;
epicState.page = 1;
renderEpicGamesList();
}, 200));
toolbar.querySelector('#sglv-epic-sort').addEventListener('change', (e) => {
epicState.sort = e.target.value;
epicState.page = 1;
renderEpicGamesList();
});
const setEpicViewMode = (mode) => {
epicState.viewMode = mode;
['card', 'cover', 'list'].forEach(m => {
toolbar.querySelector('#sglv-epic-' + m)?.classList.toggle('active', m === mode);
});
renderEpicGamesList();
};
toolbar.querySelector('#sglv-epic-card').addEventListener('click', () => setEpicViewMode('card'));
toolbar.querySelector('#sglv-epic-cover').addEventListener('click', () => setEpicViewMode('cover'));
toolbar.querySelector('#sglv-epic-list').addEventListener('click', () => setEpicViewMode('list'));
toolbar.querySelector('#sglv-epic-import').addEventListener('click', () => importEpicJson());
toolbar.querySelector('#sglv-epic-export').addEventListener('click', () => exportEpicJson());
toolbar.querySelector('#sglv-epic-refresh').addEventListener('click', () => loadEpicGames(true));
// v1.5.0: 筛选改为点击 KPI 卡片切换(再次点击同一卡片回到「全部」),与年度大作标签页一致
kpi.addEventListener('click', e => {
const card = e.target.closest('[data-epic-filter]');
if (!card) return;
const f = card.dataset.epicFilter;
epicState.ownFilter = epicState.ownFilter === f ? 'all' : f;
epicState.page = 1;
renderEpicKpi(); // 刷新 KPI active 高亮
renderEpicGamesList();
});
if (!epicState.loaded && !epicState.loading) {
loadEpicGames();
} else {
renderEpicSections();
}
}
function renderEpicSections() {
if (_collectionActiveTab !== 'epic' || !document.getElementById('sglv-epic-content')) return;
renderEpicKpi();
renderEpicGamesList();
}
function renderEpicKpi() {
const el = document.getElementById('sglv-epic-kpi');
if (!el) return;
if (!epicState.loaded) { el.innerHTML = ''; return; }
const total = epicState.appids.length;
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
const owned = epicState.appids.filter(n => isOwned(Number(n))).length;
const notOwned = total - owned;
const srcLabel = epicSourceLabel(epicState.source);
const updatedStr = epicState.updatedAt
? (Host.isZh ? '更新于 ' : 'Updated ') + epicState.updatedAt + ' · ' + srcLabel
: (Host.isZh ? '来源:' : 'Source: ') + srcLabel;
el.innerHTML = `
${total.toLocaleString()}
${updatedStr}
${owned.toLocaleString()}
${Host.T.epicKpiOwnedSub}
${notOwned.toLocaleString()}
${Host.isZh ? '可在 Epic 领取' : 'Claimable on Epic'}
`;
}
function renderEpicGamesList() {
const content = document.getElementById('sglv-epic-content');
if (!content) return;
const clearPagination = () => { const p = document.getElementById('sglv-epic-pagination'); if (p) p.innerHTML = ''; };
if (epicState.loading) {
content.innerHTML = '' + Host.T.epicLoading + '
';
clearPagination();
return;
}
if (epicState.loadError) {
content.innerHTML = '' + Host.T.epicLoadError + '
';
clearPagination();
return;
}
if (!epicState.loaded || epicState.appids.length === 0) {
content.innerHTML = '' + Host.T.epicNoData + '
';
clearPagination();
return;
}
const list = getFilteredEpic();
if (list.length === 0) {
content.innerHTML = '' + (Host.isZh ? '无匹配结果' : 'No matching results') + '
';
clearPagination();
return;
}
const { pageItems, totalPages, page: _clampedPage } = Host.paginate(list, epicState.page, epicState.pageSize);
epicState.page = _clampedPage;
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
const freeBadge = epicFreeBadgeHtml();
// 卡片视图
if (epicState.viewMode === 'card') {
content.innerHTML = '' + pageItems.map(appid => {
const name = epicDisplayName(appid);
const clickUrl = 'https://store.steampowered.com/app/' + appid;
const nameSpan = '
' + Host.wlEscape(name) + ' ' + appid + ' ';
return '
' +
'
' + freeBadge + epicPosterImg(appid, name) + '
' +
'
' +
'
' + nameSpan + '
' +
'
' + epicOwnedBadgeHtml(isOwned(appid)) + '
' +
'
' +
'
';
}).join('') + '
';
} else if (epicState.viewMode === 'cover') {
// 封面视图(横板大图)
// v2.9.22: 标签统一规则——封面上不放任何徽章(freeBadge 在右下角,owned badge 在标题下方)
content.innerHTML = '' + pageItems.map(appid => {
const name = epicDisplayName(appid);
const clickUrl = 'https://store.steampowered.com/app/' + appid;
const nameSpan = '
' + Host.wlEscape(name) + ' ' + appid + ' ';
return '
' +
'
' + freeBadge + epicCapsuleImg(appid, name) + '
' +
'
' +
'
' + nameSpan + '
' +
'
' + epicOwnedBadgeHtml(isOwned(appid)) + '
' +
'
' +
'
';
}).join('') + '
';
} else {
// 列表视图
content.innerHTML = '' + pageItems.map(appid => {
const name = epicDisplayName(appid);
const clickUrl = 'https://store.steampowered.com/app/' + appid;
const iconHtml = '
';
const nameSpan = '
' + Host.wlEscape(name) + ' ' + appid + ' ';
return '
' +
iconHtml +
'
' +
'
' + nameSpan + '
' +
'
' + freeBadge + epicOwnedBadgeHtml(isOwned(appid)) + '
' +
'
' +
'
';
}).join('') + '
';
}
// 分页
// v1.9.47-fix: 补 onJump——Epic 翻页 5 个按钮全部走 onJump(参见主脚本 renderPagination)
const pagination = document.getElementById('sglv-epic-pagination');
if (pagination) {
Host.renderPagination(pagination, 'sglv-epic', epicState.page, totalPages, list.length,
() => { epicState.page--; renderEpicGamesList(); },
() => { epicState.page++; renderEpicGamesList(); },
(p) => { epicState.page = p; renderEpicGamesList(); });
}
// 异步加载中文名 + 写入本地缓存供搜索/排序
content.querySelectorAll('[data-sglv-epic-appid]').forEach(el => {
const appid = el.getAttribute('data-sglv-epic-appid');
const original = el.textContent;
epicLoadName(appid).then(name => {
if (name && name !== original) {
el.textContent = name;
el.title = name + ' (' + original + ')';
// v2.7.9: 清除占位符内联样式,允许 CSS class 生效(项目约定)
el.style.color = '';
el.style.fontStyle = '';
el.style.fontWeight = '';
}
});
});
}
// ==================== 锁区游戏标签页 (v2.9.12) ====================
// 数据源: GitHub 静态源优先(fastly.jsdelivr.net/gh/SmallRob/steam-namespace steam_blocked_apps.json)
// → steam-tracker.com API (Banned + Purchase disabled) → HTML (Regional variant) + 手动导入
// 宿主桥接: Host.loadBlockedApps(force), Host.getBlockedApps(), Host.isBlockedLoaded(),
// Host.parseManualBlockedJson(text), Host.getBlockedSource()
const blockedState = {
apps: [], loaded: false, loading: false,
search: '', ownFilter: 'all', catFilter: 'all', sort: 'appidDesc',
page: 1, pageSize: 48, viewMode: 'card',
source: '', loadError: null,
fetchedAt: '', // v2.9.90: 远程数据集更新时间
pendingForce: false, // v2.9.90: loading中时排队的force刷新请求
// v2.9.91: 好评500分类单独加载(独立于主锁区数据,确保 categoryId=500 条目始终可用)
topRatedApps: [], topRatedLoaded: false, topRatedLoading: false,
topRatedFetchedAt: '',
refreshStage: '', // v2.9.91: 刷新进度文本
};
function blockedCatBadgeHtml(catId) {
if (catId === 20) return '' + Host.T.blockedBadgeBanned + ' ';
if (catId === 3) return '' + Host.T.blockedBadgePurchase + ' ';
if (catId === 10) return '' + Host.T.blockedBadgeRegional + ' ';
if (catId === 0) return '' + Host.T.blockedBadgeManual + ' ';
// v1.9.15: 好评500 分类(人工整理锁区游戏,categoryId=500)
if (catId === 500) return '' + (Host.isZh ? '好评500' : 'Positive 500+') + ' ';
return '';
}
function blockedOwnedBadgeHtml(owned) {
if (owned) return '' + Host.T.blockedBadgeOwned + ' ';
return '';
}
// v2.9.91: 好评500分类数据远程URL(与主脚本 BLOCKED_STATIC_URL 同源,独立加载确保 categoryId=500 条目可用)
const BLOCKED_TOP_RATED_URL = 'https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/steam_blocked_apps.json';
const BLOCKED_TOP_RATED_FALLBACK_URL = 'https://raw.githubusercontent.com/SmallRob/steam-namespace/refs/heads/main/data/steam_blocked_apps.json';
/**
* v2.9.91: 单独加载好评500分类数据
* 直接从远程 JSON 筛选 categoryId=500 条目,独立于主锁区数据缓存
* @param {boolean} force - 强制刷新(忽略永久缓存)
* @returns {Promise} 好评500条目数组
*/
function _fetchTopRatedFromRemote() {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'function') { reject(new Error('GM_xmlhttpRequest unavailable')); return; }
const tryFetch = (url, isFallback) => {
GM_xmlhttpRequest({
method: 'GET',
url,
headers: { 'Accept': 'application/json' },
timeout: 15000,
onload(r) {
if (r.status < 200 || r.status >= 300) {
if (!isFallback) { tryFetch(BLOCKED_TOP_RATED_FALLBACK_URL, true); return; }
reject(new Error(`HTTP ${r.status}`)); return;
}
try {
const data = JSON.parse(_safeRespText(r));
const apps = Array.isArray(data) ? data : (Array.isArray(data.data) ? data.data : []);
const topRated = apps
.filter(a => a.categoryId === 500 || a.category_id === 500)
.map(a => ({
appid: Number(a.appid) || 0,
name: (a.name || '').trim(),
category: a.category || '好评500',
categoryId: 500,
type: a.type || 'game',
source: 'static',
changedAt: a.changedAt || a.changed_at || '',
positiveRating: a.positiveRating || 0,
reviews: a.reviews || 0,
cheapestRegion: a.cheapestRegion || '',
}));
const fetchedAt = (!Array.isArray(data) && data.fetchedAt) ? data.fetchedAt : '';
resolve({ apps: topRated, fetchedAt });
} catch (e) {
if (!isFallback) { tryFetch(BLOCKED_TOP_RATED_FALLBACK_URL, true); return; }
reject(new Error('JSON parse fail'));
}
},
onerror: () => {
if (!isFallback) { tryFetch(BLOCKED_TOP_RATED_FALLBACK_URL, true); return; }
reject(new Error('Network error'));
},
ontimeout: () => {
if (!isFallback) { tryFetch(BLOCKED_TOP_RATED_FALLBACK_URL, true); return; }
reject(new Error('Timeout'));
},
});
};
tryFetch(BLOCKED_TOP_RATED_URL, false);
});
}
/**
* v2.9.91: 加载好评500分类数据(含永久缓存 + 后台静默刷新)
* @param {boolean} force - 强制刷新
*/
function loadTopRatedApps(force) {
if (blockedState.topRatedLoading) return Promise.resolve();
// 非强制时优先从永久缓存秒开
if (!force) {
const permCached = _permCacheGet('blockedTopRated');
if (permCached && Array.isArray(permCached.apps) && permCached.apps.length > 0) {
blockedState.topRatedApps = permCached.apps;
blockedState.topRatedFetchedAt = permCached.fetchedAt || '';
blockedState.topRatedLoaded = true;
// 后台静默刷新
_fetchTopRatedFromRemote().then(result => {
if (result.fetchedAt && result.fetchedAt !== blockedState.topRatedFetchedAt) {
blockedState.topRatedApps = result.apps;
blockedState.topRatedFetchedAt = result.fetchedAt;
_permCacheSet('blockedTopRated', { apps: result.apps, fetchedAt: result.fetchedAt });
if (_collectionActiveTab === 'blocked') renderBlockedSections();
}
}).catch(() => {});
return Promise.resolve();
}
}
blockedState.topRatedLoading = true;
return _fetchTopRatedFromRemote().then(result => {
blockedState.topRatedApps = result.apps || [];
blockedState.topRatedFetchedAt = result.fetchedAt || '';
blockedState.topRatedLoaded = true;
blockedState.topRatedLoading = false;
_permCacheSet('blockedTopRated', { apps: blockedState.topRatedApps, fetchedAt: blockedState.topRatedFetchedAt });
}).catch(() => {
blockedState.topRatedLoading = false;
});
}
/**
* v2.9.91: 将好评500条目合并到主锁区列表(按 appid 去重,已有条目不覆盖)
* @returns {Array} 合并后的完整锁区列表
*/
function getMergedBlockedApps() {
if (!blockedState.topRatedApps || blockedState.topRatedApps.length === 0) {
return blockedState.apps;
}
const existing = new Set(blockedState.apps.map(a => a.appid));
const additions = blockedState.topRatedApps.filter(a => !existing.has(a.appid));
return blockedState.apps.concat(additions);
}
function loadBlockedGames(force) {
// v2.9.90: loading中时,force请求排队(pendingForce),避免静默丢弃用户刷新操作
// v1.9.27: force 时直接重置可能卡死的 loading 标志(浏览器扩展阻断回调时 loading 可能永久卡 true)
if (blockedState.loading) {
if (force) {
blockedState.loading = false;
blockedState.pendingForce = false;
} else {
return;
}
}
blockedState.loading = true;
blockedState.loadError = null;
blockedState.refreshStage = Host.isZh ? '正在加载锁区数据…' : 'Loading blocked data…';
// v2.9.90: 刷新按钮 spinner 反馈
const refreshBtn = document.getElementById('sglv-blocked-refresh');
if (refreshBtn) refreshBtn.classList.add('sglv-spin');
renderBlockedSections();
renderBlockedToolbarStatus();
if (typeof Host.loadBlockedApps !== 'function') {
blockedState.loading = false;
blockedState.loadError = 'Host loadBlockedApps unavailable';
blockedState.refreshStage = '';
if (refreshBtn) refreshBtn.classList.remove('sglv-spin');
renderBlockedSections();
renderBlockedToolbarStatus();
return;
}
// v2.9.91: 同时加载好评500分类数据(force 时同步强制刷新)
loadTopRatedApps(force);
Host.loadBlockedApps(force, (stage, text) => {
// v2.9.91: 进度回调更新工具栏状态
blockedState.refreshStage = text || '';
renderBlockedToolbarStatus();
}).then(result => {
blockedState.apps = result.apps || [];
blockedState.source = result.source || 'api';
// v2.9.90: 存储 fetchedAt 用于显示数据更新时间
blockedState.fetchedAt = result.fetchedAt || '';
blockedState.loaded = true;
blockedState.loading = false;
blockedState.page = 1;
blockedState.refreshStage = '';
// v1.9.5: 写入永久缓存(永不过期)+ v2.9.90: 含 fetchedAt
_permCacheSet('blockedApps', { apps: blockedState.apps, source: blockedState.source, fetchedAt: blockedState.fetchedAt });
if (refreshBtn) refreshBtn.classList.remove('sglv-spin');
renderBlockedSections();
renderBlockedToolbarStatus();
// v2.9.90: 若有排队的 force 请求,立即执行
if (blockedState.pendingForce) {
blockedState.pendingForce = false;
loadBlockedGames(true);
}
}).catch(e => {
blockedState.loading = false;
blockedState.loadError = e.message || 'Load failed';
blockedState.refreshStage = '';
if (refreshBtn) refreshBtn.classList.remove('sglv-spin');
renderBlockedSections();
renderBlockedToolbarStatus();
// v2.9.90: 若有排队的 force 请求,立即执行
if (blockedState.pendingForce) {
blockedState.pendingForce = false;
loadBlockedGames(true);
}
});
}
function importBlockedJson() {
const text = prompt(Host.T.blockedImportPrompt);
if (!text || !text.trim()) return;
try {
const apps = Host.parseManualBlockedJson(text);
if (!apps.length) { Host.showToast(Host.T.blockedImportNoData); return; }
// 合并到现有数据(手动导入的不覆盖已有条目)
const existing = new Map(blockedState.apps.map(a => [a.appid, a]));
apps.forEach(a => { if (!existing.has(a.appid)) existing.set(a.appid, a); });
blockedState.apps = Array.from(existing.values());
blockedState.loaded = true;
blockedState.source = 'import';
blockedState.page = 1;
// v1.9.5: 写入永久缓存(永不过期)+ TTL 缓存(兼容主脚本加载路径)
// v2.9.90: 含 fetchedAt(手动导入无远程时间戳,置空)
_permCacheSet('blockedApps', { apps: blockedState.apps, source: 'import', fetchedAt: '' });
try { Host.cacheSet('blockedApps', { apps: blockedState.apps, source: 'import', fetchedAt: '' }, Host.cacheTTL.blockedApps); } catch (e) { /* 忽略失败 */ }
Host.showToast(Host.T.blockedImportOk.replace('{n}', apps.length));
renderBlockedSections();
} catch (e) {
Host.showToast(Host.T.blockedImportFail + e.message);
}
}
function getFilteredBlocked() {
const q = blockedState.search.toLowerCase().trim();
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
// v2.9.91: 使用合并后的锁区列表(含好评500单独加载的条目)
let list = getMergedBlockedApps().filter(a => a.appid > 0);
if (q) {
list = list.filter(a => {
if (String(a.appid).includes(q)) return true;
return (a.name || '').toLowerCase().includes(q);
});
}
if (blockedState.ownFilter === 'owned') list = list.filter(a => isOwned(a.appid));
else if (blockedState.ownFilter === 'notOwned') list = list.filter(a => !isOwned(a.appid));
if (blockedState.catFilter !== 'all') {
const catMap = { banned: 20, purchase: 3, regional: 10, manual: 0, topRated: 500 };
const targetId = catMap[blockedState.catFilter];
list = list.filter(a => a.categoryId === targetId);
}
// v1.9.5: 使用缓存的 _nameCollator,避免每次渲染重新构造
list.sort((a, b) => {
let r = 0;
switch (blockedState.sort) {
case 'appidAsc': r = a.appid - b.appid; break;
case 'nameAsc': r = _nameCollator.compare(a.name || '~', b.name || '~') || (a.appid - b.appid); break;
case 'dateDesc': r = (b.changedAt || '').localeCompare(a.changedAt || ''); break;
case 'dateAsc': r = (a.changedAt || '').localeCompare(b.changedAt || ''); break;
case 'appidDesc':
default: r = b.appid - a.appid;
}
return r || (a.appid - b.appid);
});
return list;
}
function renderBlockedTab(parent) {
const wrap = document.createElement('div');
wrap.className = 'sglv-psplus-wrap';
const kpi = document.createElement('div');
kpi.id = 'sglv-blocked-kpi';
wrap.appendChild(kpi);
const toolbar = document.createElement('div');
toolbar.className = 'sglv-toolbar';
const sortOptions = [
{ key: 'appidDesc', label: Host.T.blockedSortAppidDesc },
{ key: 'appidAsc', label: Host.T.blockedSortAppidAsc },
{ key: 'nameAsc', label: Host.T.blockedSortNameAsc },
{ key: 'dateDesc', label: Host.T.blockedSortDateDesc },
{ key: 'dateAsc', label: Host.T.blockedSortDateAsc },
];
toolbar.innerHTML = `
${sortOptions.map(o => `${o.label} `).join('')}
${Host.ICONS.grid}
${Host.ICONS.cover}
${Host.ICONS.list}
${Host.ICONS.tag}
${Host.ICONS.refresh}
`;
wrap.appendChild(toolbar);
const filterBar = document.createElement('div');
filterBar.className = 'sglv-filter-bar';
const ownBtns = [
{ key: 'all', label: Host.T.blockedFilterAll },
{ key: 'owned', label: Host.T.blockedFilterOwned },
{ key: 'notOwned', label: Host.T.blockedFilterNotOwned },
];
const catBtns = [
{ key: 'all', label: Host.T.blockedFilterAll },
{ key: 'banned', label: Host.T.blockedBadgeBanned },
{ key: 'purchase', label: Host.T.blockedBadgePurchase },
{ key: 'regional', label: Host.T.blockedBadgeRegional },
{ key: 'manual', label: Host.T.blockedBadgeManual },
{ key: 'topRated', label: Host.isZh ? '好评500' : 'Positive 500+' },
];
filterBar.innerHTML = `
${ownBtns.map(b => `${b.label} `).join('')}
${catBtns.map(b => `${b.label} `).join('')}
`;
wrap.appendChild(filterBar);
const content = document.createElement('div');
content.className = 'sglv-content';
content.id = 'sglv-blocked-content';
wrap.appendChild(content);
parent.appendChild(wrap);
toolbar.querySelector('#sglv-blocked-search').addEventListener('input', _debounce((e) => {
blockedState.search = e.target.value; blockedState.page = 1; renderBlockedGamesList();
}, 200));
toolbar.querySelector('#sglv-blocked-sort').addEventListener('change', (e) => {
blockedState.sort = e.target.value; blockedState.page = 1; renderBlockedGamesList();
});
const setBlockedViewMode = (mode) => {
blockedState.viewMode = mode;
['card', 'cover', 'list'].forEach(m => { toolbar.querySelector('#sglv-blocked-' + m)?.classList.toggle('active', m === mode); });
renderBlockedGamesList();
};
toolbar.querySelector('#sglv-blocked-card').addEventListener('click', () => setBlockedViewMode('card'));
toolbar.querySelector('#sglv-blocked-cover').addEventListener('click', () => setBlockedViewMode('cover'));
toolbar.querySelector('#sglv-blocked-list').addEventListener('click', () => setBlockedViewMode('list'));
toolbar.querySelector('#sglv-blocked-import').addEventListener('click', () => importBlockedJson());
toolbar.querySelector('#sglv-blocked-refresh').addEventListener('click', () => loadBlockedGames(true));
filterBar.querySelectorAll('.sglv-blocked-own-btn').forEach(btn => {
btn.addEventListener('click', () => {
blockedState.ownFilter = btn.dataset.own; blockedState.page = 1;
filterBar.querySelectorAll('.sglv-blocked-own-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderBlockedKpi(); // v1.6.0: 同步 KPI active 高亮
renderBlockedGamesList();
});
});
filterBar.querySelectorAll('.sglv-blocked-cat-btn').forEach(btn => {
btn.addEventListener('click', () => {
blockedState.catFilter = btn.dataset.cat; blockedState.page = 1;
filterBar.querySelectorAll('.sglv-blocked-cat-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderBlockedGamesList();
});
});
// v1.6.0: 筛选改为点击 KPI 卡片切换(再次点击同一卡片回到「全部」),与年度大作/Epic赠送标签页一致
kpi.addEventListener('click', e => {
const card = e.target.closest('[data-blocked-filter]');
if (!card) return;
const f = card.dataset.blockedFilter;
blockedState.ownFilter = blockedState.ownFilter === f ? 'all' : f;
blockedState.page = 1;
syncBlockedFilterBtns(filterBar); // 同步筛选栏按钮 active 状态
renderBlockedKpi(); // 刷新 KPI active 高亮
renderBlockedGamesList();
});
if (!blockedState.loaded && !blockedState.loading) {
// 优先从宿主缓存读取
if (Host.isBlockedLoaded && Host.isBlockedLoaded() && Host.getBlockedApps) {
blockedState.apps = Host.getBlockedApps();
blockedState.source = Host.getBlockedSource ? Host.getBlockedSource() : 'api';
// v2.9.90: 读取宿主的 fetchedAt
if (Host.getBlockedFetchedAt) blockedState.fetchedAt = Host.getBlockedFetchedAt();
blockedState.loaded = true;
renderBlockedSections();
} else {
// v1.9.5: 宿主缓存无数据 → 尝试永久缓存秒开,后台静默刷新
const permCached = _permCacheGet('blockedApps');
if (permCached && Array.isArray(permCached.apps) && permCached.apps.length > 0) {
blockedState.apps = permCached.apps;
blockedState.source = permCached.source || 'api';
// v2.9.90: 读取 permCache 中的 fetchedAt
blockedState.fetchedAt = permCached.fetchedAt || '';
blockedState.loaded = true;
renderBlockedSections();
loadBlockedGames(false); // 后台静默刷新
} else {
loadBlockedGames(false);
}
}
} else {
renderBlockedSections();
}
// v2.9.91: 单独加载好评500分类数据(独立于主锁区数据缓存,确保 categoryId=500 条目始终可用)
if (!blockedState.topRatedLoaded && !blockedState.topRatedLoading) {
loadTopRatedApps(false).then(() => {
if (_collectionActiveTab === 'blocked') renderBlockedSections();
});
}
}
function renderBlockedSections() {
if (_collectionActiveTab !== 'blocked' || !document.getElementById('sglv-blocked-content')) return;
renderBlockedKpi();
renderBlockedToolbarStatus();
renderBlockedGamesList();
}
// v2.9.91: 渲染工具栏刷新按钮右侧的状态文本(更新时间 / 加载进度)
function renderBlockedToolbarStatus() {
const el = document.getElementById('sglv-blocked-status');
if (!el) return;
if (blockedState.refreshStage) {
// 加载中:显示进度文本 + spinner
el.innerHTML = ` ${Host.wlEscape(blockedState.refreshStage)}`;
el.className = 'sglv-blocked-status loading';
} else if (blockedState.fetchedAt) {
// 空闲:显示数据更新时间
const date = blockedState.fetchedAt.slice(0, 10);
el.textContent = (Host.isZh ? '更新: ' : 'Updated: ') + date;
el.className = 'sglv-blocked-status';
} else {
el.textContent = '';
el.className = 'sglv-blocked-status';
}
}
// v1.6.0: KPI 点击后同步筛选栏 ownFilter 按钮 active 状态
function syncBlockedFilterBtns(filterBar) {
filterBar.querySelectorAll('.sglv-blocked-own-btn').forEach(b => {
b.classList.toggle('active', b.dataset.own === blockedState.ownFilter);
});
}
function renderBlockedKpi() {
const el = document.getElementById('sglv-blocked-kpi');
if (!el) return;
if (!blockedState.loaded) { el.innerHTML = ''; return; }
// v2.9.91: 使用合并后的锁区列表(含好评500单独加载的条目)
const mergedApps = getMergedBlockedApps();
const total = mergedApps.length;
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
const owned = mergedApps.filter(a => isOwned(a.appid)).length;
const notOwned = total - owned;
const banned = mergedApps.filter(a => a.categoryId === 20).length;
const purchase = mergedApps.filter(a => a.categoryId === 3).length;
const regional = mergedApps.filter(a => a.categoryId === 10).length;
// v1.9.15: 好评500 分类计数(非零时追加到副标题)
const topRated = mergedApps.filter(a => a.categoryId === 500).length;
const subText = Host.T.blockedKpiSubBanned.replace('N', banned).replace('M', purchase).replace('K', regional) +
(topRated > 0 ? ' · ' + (Host.isZh ? '好评500 ' : 'Positive500 ') + topRated : '');
const tipAll = Host.isZh ? '点击筛选:全部' : 'Click to filter: All';
const tipOwned = Host.isZh ? '点击筛选:steam在库' : 'Click to filter: In Library';
const tipNotOwned = Host.isZh ? '点击筛选:未拥有' : 'Click to filter: Not Owned';
el.innerHTML = `
${total.toLocaleString()}
${subText}
${owned.toLocaleString()}
${Host.isZh ? '已在 Steam 库' : 'In Steam library'}
${notOwned.toLocaleString()}
${Host.isZh ? '待探索' : 'To explore'}
`;
}
function renderBlockedGamesList() {
const content = document.getElementById('sglv-blocked-content');
if (!content) return;
const clearPagination = () => { const p = document.getElementById('sglv-blocked-pagination'); if (p) p.innerHTML = ''; };
if (blockedState.loading) {
// v2.9.91: 显示刷新进度文本(若有)
const stageText = blockedState.refreshStage || Host.T.blockedLoading;
content.innerHTML = '' + Host.wlEscape(stageText) + '
';
clearPagination(); return;
}
if (blockedState.loadError) {
content.innerHTML = '' + Host.T.blockedLoadError + '
';
clearPagination(); return;
}
if (!blockedState.loaded || blockedState.apps.length === 0) {
content.innerHTML = '' + Host.T.blockedNoData + '
';
clearPagination(); return;
}
const list = getFilteredBlocked();
if (list.length === 0) {
content.innerHTML = '' + (Host.isZh ? '无匹配结果' : 'No matching results') + '
';
clearPagination(); return;
}
const { pageItems, totalPages, page: _clampedPage } = Host.paginate(list, blockedState.page, blockedState.pageSize);
blockedState.page = _clampedPage;
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
if (blockedState.viewMode === 'card') {
content.innerHTML = '' + pageItems.map(a => {
const name = a.name || ('App ' + a.appid);
const clickUrl = 'https://store.steampowered.com/app/' + a.appid;
const owned = isOwned(a.appid);
const nameSpan = '
' + Host.wlEscape(name) + ' ' + a.appid + ' ';
const catBadge = blockedCatBadgeHtml(a.categoryId);
const ownedBadge = blockedOwnedBadgeHtml(owned);
const dateStr = a.changedAt ? '
' + Host.T.blockedChangedAt + ': ' + Host.wlEscape(a.changedAt) + ' ' : '';
return '
' +
'
' + blockedPosterImg(a.appid, name) + '
' +
'
' +
'
' + nameSpan + ' ' + dateStr + '
' +
'
' + catBadge + ownedBadge + '
' +
'
' +
'
';
}).join('') + '
';
} else if (blockedState.viewMode === 'cover') {
// v2.9.22: 标签统一规则——封面上不放任何徽章(cat/owned 全部移到标题下方)
content.innerHTML = '' + pageItems.map(a => {
const name = a.name || ('App ' + a.appid);
const clickUrl = 'https://store.steampowered.com/app/' + a.appid;
const owned = isOwned(a.appid);
const catBadge = blockedCatBadgeHtml(a.categoryId);
const ownedBadge = blockedOwnedBadgeHtml(owned);
return '
' +
'
' + blockedCapsuleImg(a.appid, name) + '
' +
'
' +
'
' + Host.wlEscape(name) + ' ' + a.appid + '
' +
'
' + catBadge + ownedBadge + '
' +
'
' +
'
';
}).join('') + '
';
} else {
content.innerHTML = '' + pageItems.map(a => {
const name = a.name || ('App ' + a.appid);
const clickUrl = 'https://store.steampowered.com/app/' + a.appid;
const owned = isOwned(a.appid);
const iconHtml = '
';
const catBadge = blockedCatBadgeHtml(a.categoryId);
const ownedBadge = blockedOwnedBadgeHtml(owned);
const dateStr = a.changedAt ? '
' + Host.T.blockedChangedAt + ': ' + Host.wlEscape(a.changedAt) + ' ' : '';
return '
' +
iconHtml +
'
' +
'
' + Host.wlEscape(name) + ' ' + a.appid + '
' +
'
' + catBadge + ownedBadge + ' ' + dateStr + '
' +
'
' +
'
';
}).join('') + '
';
}
const pagination = document.getElementById('sglv-blocked-pagination');
if (pagination) {
// v1.9.47-fix: 补 onJump——锁区翻页 5 个按钮全部走 onJump(参见主脚本 renderPagination)
Host.renderPagination(pagination, 'sglv-blocked', blockedState.page, totalPages, list.length,
() => { blockedState.page--; renderBlockedGamesList(); },
() => { blockedState.page++; renderBlockedGamesList(); },
(p) => { blockedState.page = p; renderBlockedGamesList(); });
}
}
// ==================== 年度大作标签页 (v2.9.7, v1.7.x 按年数据源) ====================
// v1.8.1: 数据源改用 jsdelivr CDN 优先(国内稳定),raw.githubusercontent 兜底。
// raw.githubusercontent.com 在国内经常连接超时/被墙,导致年度大作加载失败。
// jsdelivr CDN 对 GitHub 仓库有镜像缓存,国内访问稳定且速度快。
// 采用三级回退链:fastly.jsdelivr → cdn.jsdelivr → raw.githubusercontent
// 缓存延长至 6 天,刷新节流延长至 24h(数据基本不变,偶尔新增待发售游戏)。
// v1.7.1: GitHub repo 实际只存了一个 2026.json 全量文件(含 2020-2026 所有年度),
// 按年探测 2024/2025.json 都会 404 导致大量数据被静默丢弃。
// 改为"单文件全量拉 2026.json + 从 date 字段动态提取 availableYears"。
// JSON 格式:[{ name: "游戏名", date: "2026年01月30日", appid: 2362060 }, ...]
// v1.7.0: 增加按年度筛选——availableYears 从数据里动态提取(去重 + 倒序)
// 默认展示全部年度 + 按时间降序(dateDesc)
const GOTY_YEAR_MIN = 2020; // 数据里允许的最小年份(用于过滤异常数据)
const GOTY_DATA_FILE = '2026.json'; // 全量文件名(repo 实际只这一个)
// v1.8.1: CDN 回退链——优先 jsdelivr(国内稳定),raw.githubusercontent 兜底
const GOTY_DATA_SOURCES = [
'https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/' + GOTY_DATA_FILE, // ① fastly 节点(优先)
'https://cdn.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/' + GOTY_DATA_FILE, // ② jsdelivr 主节点
'https://raw.githubusercontent.com/SmallRob/steam-namespace/refs/heads/main/data/' + GOTY_DATA_FILE, // ③ raw 兜底
];
function _gotyCurrentYear() { return new Date().getFullYear(); }
// "2026年01月30日" / "2026 年7月8日" → "20260130"(可排序字符串)
function gotyDateToSortable(dateStr) {
if (!dateStr) return '99999999';
const m = String(dateStr).match(/(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日/);
if (!m) return '99999999';
return m[1] + m[2].padStart(2, '0') + m[3].padStart(2, '0');
}
// 从可排序字符串中提取年份("20260130" → 2026)
function gotyDateYear(sortable) {
if (!sortable || sortable === '99999999') return null;
return parseInt(sortable.slice(0, 4), 10);
}
// v2.9.22: "2026年01月30日" → "2026-01-30"(完整年月日,紧凑显示用)
function gotyDateFull(dateStr) {
const s = gotyDateToSortable(dateStr);
if (s === '99999999') return dateStr || '';
return s.slice(0, 4) + '-' + s.slice(4, 6) + '-' + s.slice(6, 8);
}
// ==================== v1.4.0: 年度大作封面获取优化 ====================
// 参考 Steam-Wishlist-Sidebar v1.2.0(SWE)的「持久化 good URL 缓存 + 多 CDN 回退 +
// appdetails API 兜底」模式。年度大作多为新发售/未发售的 2026 游戏,部分胶囊图在单一
// CDN 上尚未就绪,需多 CDN 回退 + API 兜底才能稳定获取;首次会话解析成功的 URL 持久化
// 到 GM 存储(跨会话 LRU),后续会话直接命中最终 URL,跳过整条回退链,减少封面请求与闪烁。
const GOTY_COVER_CACHE_KEY = 'sglv_goty_cover_good_v1';
const GOTY_COVER_MAX = 600; // LRU 容量上限
const GOTY_COVER_TTL = 30 * 24 * 3600 * 1000; // 30 天
const _gotyCoverGood = new Map(); // `${kind}:${appid}` → 已验证可用 URL(内存)
const _gotyCoverNext = new WeakMap(); // img → 下一个待尝试的 chain 索引
const _gotyCoverApiTried = new WeakMap();// img → 是否已尝试 API 兜底
let _gotyCoverPersisted = null; // null=未加载, {key:{url,ts}}
let _gotyCoverWriteTimer = null;
let _gotyCoverDirty = false;
const _GOTY_PLACEHOLDER = "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 300'%3E%3Crect fill='%231a1f3a' width='200' height='300'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%2364748b' font-family='system-ui' font-size='16'%3E🎮%3C/text%3E%3C/svg%3E";
function _gotyCoverLoadPersisted() {
if (_gotyCoverPersisted !== null) return;
_gotyCoverPersisted = {};
try {
const raw = Host.cacheGet(GOTY_COVER_CACHE_KEY);
if (raw && typeof raw === 'object') _gotyCoverPersisted = raw;
} catch (e) { /* ignore */ }
// 同步预热内存缓存
for (const k in _gotyCoverPersisted) {
const ent = _gotyCoverPersisted[k];
if (ent && ent.url) _gotyCoverGood.set(k, ent.url);
}
}
function _gotyCoverRecordGood(appid, url, kind) {
if (!appid || !url) return;
const key = `${kind || 'poster'}:${appid}`;
if (_gotyCoverGood.get(key) === url) return; // 已记录,跳过
_gotyCoverGood.set(key, url);
_gotyCoverLoadPersisted();
_gotyCoverPersisted[key] = { url, ts: Date.now() };
// LRU 淘汰
const keys = Object.keys(_gotyCoverPersisted);
if (keys.length > GOTY_COVER_MAX) {
keys.sort((a, b) => (_gotyCoverPersisted[a].ts || 0) - (_gotyCoverPersisted[b].ts || 0));
while (keys.length > GOTY_COVER_MAX) delete _gotyCoverPersisted[keys.shift()];
}
_gotyCoverDirty = true;
// 节流落盘(80ms),避免高频 load 事件触发 GM 写入风暴
if (_gotyCoverWriteTimer) return;
_gotyCoverWriteTimer = setTimeout(() => {
_gotyCoverWriteTimer = null;
if (!_gotyCoverDirty) return;
_gotyCoverDirty = false;
try { Host.cacheSet(GOTY_COVER_CACHE_KEY, _gotyCoverPersisted, GOTY_COVER_TTL); } catch (e) { /* ignore */ }
}, 80);
}
function _gotyCoverGetGood(appid, kind) {
_gotyCoverLoadPersisted();
return _gotyCoverGood.get(`${kind || 'poster'}:${appid}`) || '';
}
// 生成年度大作封面 (无内联 onerror,回退由 _bindGotyCoverDelegation 委托处理)
// kind: 'poster'(竖版卡片) | 'capsule'(横版封面) | 'header'(列表图标)
// v1.9.41: 链走 coverChainLang(语言变体链;good 缓存仍用 GOTY 专用持久缓存)
function gotyCoverImgTag(appid, name, kind, cls) {
const good = _gotyCoverGetGood(appid, kind) || coverGoodUrl(appid, kind);
const chain = coverChainLang(appid, kind);
const firstUrl = good || chain[0];
return ` `;
}
function _gotyCoverPlaceholder(img) {
img.removeAttribute('data-goty-img'); // 占位图不再触发回退
img.src = _GOTY_PLACEHOLDER;
}
// 在年度大作内容容器上绑定图片回退委托(img 的 error/load 不冒泡,用捕获阶段)
function _bindGotyCoverDelegation(contentEl) {
if (!contentEl || contentEl.dataset.gotyCoverBound === '1') return;
contentEl.dataset.gotyCoverBound = '1';
contentEl.addEventListener('error', (e) => {
const img = e.target;
if (!img || img.tagName !== 'IMG' || !img.dataset.gotyImg) return;
const appid = img.dataset.appid;
const kind = img.dataset.gotyImg;
const chain = coverChainLang(appid, kind);
let next = _gotyCoverNext.get(img);
if (next === undefined) next = 0;
// 跳过与当前已失败 src 相同的 chain 条目,避免重复请求
const curSrc = img.getAttribute('src');
while (next < chain.length && chain[next] === curSrc) next++;
if (next < chain.length) {
_gotyCoverNext.set(img, next + 1);
img.src = chain[next];
return;
}
// 回退链耗尽 → appdetails API 兜底(仅尝试一次),成功会触发 load → 记录 good URL
// v1.9.29: 传递 kind 参数,使 API 兜底按图片类型选择最佳 URL
if (!_gotyCoverApiTried.get(img)) {
_gotyCoverApiTried.set(img, true);
Host.fetchCoverApiUrl(appid, kind).then(url => {
if (url) img.src = url;
else _gotyCoverPlaceholder(img);
}).catch(() => _gotyCoverPlaceholder(img));
} else {
_gotyCoverPlaceholder(img);
}
}, true);
contentEl.addEventListener('load', (e) => {
const img = e.target;
if (!img || img.tagName !== 'IMG' || !img.dataset.gotyImg) return;
if (img.naturalWidth && img.naturalWidth > 1) {
_gotyCoverRecordGood(img.dataset.appid, img.getAttribute('src'), img.dataset.gotyImg);
}
}, true);
}
// v1.9.5: 缓存永不过期——优先读永久缓存秒开,后台增量合并刷新(24h 节流)
const _GOTY_BG_INTERVAL = 24 * 3600 * 1000; // 年度大作数据变化慢,24h 节流足够
let _gotyLastBgRefreshAt = 0;
function loadGotyGames(force) {
// v1.9.27: force 时重置可能卡死的 loading 标志
if (gotyState.loading) {
if (!force) return;
gotyState.loading = false;
}
if (!force) {
// v1.9.5: 优先读永久缓存(永不过期)
const cached = _permCacheGet('gotyGames_v2');
if (cached && Array.isArray(cached.games) && cached.games.length > 0) {
gotyState.games = cached.games;
gotyState.updatedAt = cached.updatedAt || '';
gotyState.source = cached.source || Host.T.gotySourceCache;
gotyState.availableYears = Array.isArray(cached.availableYears) ? cached.availableYears : [];
gotyState.lastFetchAt = cached.lastFetchAt || 0;
gotyState.loaded = true;
renderGotySections();
// v1.9.5: 后台增量刷新(24h 节流)
if (_shouldThrottle(_gotyLastBgRefreshAt, _GOTY_BG_INTERVAL)) return;
_refreshGotyFromRemote(false, false);
return;
}
}
// v1.9.47-fix: 无缓存首次进入 → showLoading=true 展示 loading 骨架,避免
// "打开 GOTY tab → 远程源未返回时看到"暂无数据" → 数据到达后突然刷出" 的闪烁/误判。
_refreshGotyFromRemote(force, true);
}
function _gotyFailLoading(msg) {
gotyState.loading = false;
if (gotyState.games.length === 0) {
gotyState.loadError = msg;
} else {
gotyState.loadError = null;
Host.showToast(Host.isZh ? '更新失败,已使用本地数据' : 'Update failed, using local data');
}
renderGotyGamesList();
}
// v1.8.1: 远程获取——CDN 三级回退链(fastly.jsdelivr → cdn.jsdelivr → raw.githubusercontent)
// 依次尝试每个数据源,任一成功即停止;全部失败才报错。
// raw.githubusercontent.com 在国内经常超时/被墙,jsdelivr CDN 更稳定。
// v1.7.1: 单文件全量拉取 2026.json(包含 2020-currentYear 所有年度)
// 从每个 game 的 date 字段动态提取 availableYears(去重 + 倒序)
function _refreshGotyFromRemote(force, showLoading) {
if (typeof GM_xmlhttpRequest !== 'function') {
if (showLoading) _gotyFailLoading('noXhr');
return;
}
// v1.9.27: force 时重置可能卡死的 loading 标志
if (gotyState.loading) {
if (!force) return;
gotyState.loading = false;
}
if (showLoading) { gotyState.loading = true; gotyState.loadError = null; renderGotyGamesList(); }
const targetYear = gotyState.year; // 用户已选年度(null = 全部)
// v1.8.1: 处理成功响应的公共逻辑(解析 + 过滤 + 去重 + 缓存 + 渲染)
function _processGotyResponse(r) {
const _gotyTxt = _safeRespText(r);
if (r.status === 404 || !_gotyTxt) return false; // 交给下一个源
const data = JSON.parse(_gotyTxt);
if (!Array.isArray(data) || data.length === 0) return false;
// 解析 + 过滤无效数据
const games = data
.filter(g => g && g.appid && Number(g.appid) > 0)
.map(g => {
const sortable = gotyDateToSortable(g.date || '');
return { appid: Number(g.appid), name: g.name || ('App ' + g.appid), date: g.date || '', year: gotyDateYear(sortable) };
})
.filter(g => g.year && g.year >= GOTY_YEAR_MIN && g.year <= _gotyCurrentYear() + 2);
if (games.length === 0) return false;
// v1.9.5: 增量合并去重(按 appid)——使用统一工具函数
gotyState.games = _mergeByAppid(gotyState.games, games);
// 从所有 games 的 date 字段动态提取 availableYears(去重 + 倒序)
const yearSet = new Set();
gotyState.games.forEach(g => { if (g.year) yearSet.add(g.year); });
gotyState.availableYears = Array.from(yearSet).sort((a, b) => b - a);
// v1.9.24: 默认展开全部年度供用户筛选(null = 全部),仅在用户已选年度不在列表时回退到全部
if (targetYear && !gotyState.availableYears.includes(targetYear)) {
gotyState.year = null;
}
gotyState.updatedAt = new Date().toISOString().slice(0, 10);
gotyState.source = Host.T.gotySourceGithub;
gotyState.loaded = true;
gotyState.loadError = null;
gotyState.loading = false;
gotyState.lastFetchAt = Date.now();
_gotyLastBgRefreshAt = Date.now(); // v1.9.5: 记录后台刷新时间戳
// v1.9.5: 写入永久缓存(永不过期)
_permCacheSet('gotyGames_v2', { games: gotyState.games, year: gotyState.year, availableYears: gotyState.availableYears, updatedAt: gotyState.updatedAt, source: gotyState.source, lastFetchAt: gotyState.lastFetchAt });
renderGotySections();
return true;
}
// v1.8.1: 依次尝试 GOTY_DATA_SOURCES 中的每个数据源
function _tryNextSource(idx) {
if (idx >= GOTY_DATA_SOURCES.length) {
// 所有数据源都失败
gotyState.loading = false;
if (gotyState.games.length === 0) _gotyFailLoading('networkError');
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: GOTY_DATA_SOURCES[idx],
timeout: 15000,
onload(r) {
try {
if (_processGotyResponse(r)) return; // 成功,停止
} catch (e) { /* 解析异常,交给下一个源 */ }
_tryNextSource(idx + 1); // 失败,尝试下一个源
},
onerror() { _tryNextSource(idx + 1); },
ontimeout() { _tryNextSource(idx + 1); },
});
}
_tryNextSource(0);
}
function getFilteredGoty() {
const q = gotyState.search.toLowerCase().trim();
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
let list = gotyState.games.filter(g => g.appid > 0);
if (q) {
list = list.filter(g => String(g.appid).includes(q) || (g.name || '').toLowerCase().includes(q));
}
// v1.7.1: 按年度筛选(默认 null = 全部年度);games 已在 _refreshGotyFromRemote 中预解析 year
if (gotyState.year) {
list = list.filter(g => g.year === gotyState.year);
}
if (gotyState.ownFilter === 'owned') list = list.filter(g => isOwned(g.appid));
else if (gotyState.ownFilter === 'notOwned') list = list.filter(g => !isOwned(g.appid));
// v1.7.0: 默认 sort='dateDesc'——按时间降序(最新在前)
list.sort((a, b) => {
const da = gotyDateToSortable(a.date);
const db = gotyDateToSortable(b.date);
switch (gotyState.sort) {
case 'dateDesc': return db.localeCompare(da) || (b.appid - a.appid);
case 'dateAsc': return da.localeCompare(db) || (a.appid - b.appid);
case 'nameAsc': return _nameCollator.compare(a.name || '~', b.name || '~') || da.localeCompare(db);
case 'appidAsc': return a.appid - b.appid;
default: return da.localeCompare(db) || (a.appid - b.appid);
}
});
return list;
}
function gotyBadgeHtml() {
return '' + Host.T.gotyBadgeGoty + ' ';
}
function gotyOwnedBadgeHtml(owned) {
return owned
? '✓ ' + Host.T.steamInLib + ' '
: '' + Host.T.gotyBadgeNotOwned + ' ';
}
function renderGotyTab(parent) {
const wrap = document.createElement('div');
wrap.className = 'sglv-psplus-wrap';
const kpi = document.createElement('div');
kpi.id = 'sglv-goty-kpi';
wrap.appendChild(kpi);
// v1.7.0: 年度筛选 chip bar——在 toolbar 上方一行,默认"全部"active
const yearBar = document.createElement('div');
yearBar.className = 'sglv-goty-year-bar';
const renderYearBar = () => {
const yearAllLabel = (Host.T && Host.T.gotyFilterAllYears) || (Host.isZh ? '全部年度' : 'All Years');
const yearChips = [`${yearAllLabel} `];
(gotyState.availableYears || []).slice().sort((a, b) => b - a).forEach(y => {
yearChips.push(`${y} `);
});
yearBar.innerHTML = yearChips.join('');
};
renderYearBar();
wrap.appendChild(yearBar);
const toolbar = document.createElement('div');
toolbar.className = 'sglv-toolbar';
const sortOptions = [
{ key: 'dateDesc', label: Host.T.gotySortDateDesc }, // v1.7.0: 默认时间降序(最新在前)
{ key: 'dateAsc', label: Host.T.gotySortDateAsc },
{ key: 'nameAsc', label: Host.T.gotySortNameAsc },
{ key: 'appidAsc', label: Host.T.gotySortAppidAsc },
];
toolbar.innerHTML = `
${sortOptions.map(o => `${o.label} `).join('')}
${Host.ICONS.grid}
${Host.ICONS.cover}
${Host.ICONS.list}
${Host.ICONS.refresh}
`;
wrap.appendChild(toolbar);
const content = document.createElement('div');
content.className = 'sglv-content';
content.id = 'sglv-goty-content';
wrap.appendChild(content);
parent.appendChild(wrap);
toolbar.querySelector('#sglv-goty-search').addEventListener('input', _debounce((e) => {
gotyState.search = e.target.value;
gotyState.page = 1;
renderGotyGamesList();
}, 200));
toolbar.querySelector('#sglv-goty-sort').addEventListener('change', (e) => {
gotyState.sort = e.target.value;
gotyState.page = 1;
renderGotyGamesList();
});
// v1.7.0: 年度筛选事件——切换 / 拉取新数据
yearBar.addEventListener('click', (e) => {
const chip = e.target.closest('.sglv-goty-year-chip');
if (!chip) return;
const y = chip.dataset.year;
const newYear = y ? parseInt(y, 10) : null;
if (newYear === gotyState.year) return;
gotyState.year = newYear;
gotyState.page = 1;
// 切换"全部"时不需要重新拉;切换到具体年份时,如已有缓存就无需重拉
renderYearBar();
if (newYear !== null && !gotyState.availableYears.includes(newYear)) {
// 该年还没拉过,触发拉取
_refreshGotyFromRemote(false, true);
} else {
renderGotySections();
}
});
const setGotyViewMode = (mode) => {
gotyState.viewMode = mode;
['card', 'cover', 'list'].forEach(m => {
toolbar.querySelector('#sglv-goty-' + m)?.classList.toggle('active', m === mode);
});
renderGotyGamesList();
};
toolbar.querySelector('#sglv-goty-card').addEventListener('click', () => setGotyViewMode('card'));
toolbar.querySelector('#sglv-goty-cover').addEventListener('click', () => setGotyViewMode('cover'));
toolbar.querySelector('#sglv-goty-list').addEventListener('click', () => setGotyViewMode('list'));
toolbar.querySelector('#sglv-goty-refresh').addEventListener('click', () => loadGotyGames(true));
// v1.4.0: 筛选改为点击 KPI 卡片切换(再次点击同一卡片回到「全部」),与库存标签页一致
kpi.addEventListener('click', e => {
const card = e.target.closest('[data-goty-filter]');
if (!card) return;
const f = card.dataset.gotyFilter;
gotyState.ownFilter = gotyState.ownFilter === f ? 'all' : f;
gotyState.page = 1;
renderGotyKpi(); // 刷新 KPI active 高亮
renderGotyGamesList();
});
if (!gotyState.loaded && !gotyState.loading) {
loadGotyGames();
} else {
renderGotySections();
}
}
function renderGotySections() {
if (_collectionActiveTab !== 'goty' || !document.getElementById('sglv-goty-content')) return;
renderGotyKpi();
renderGotyGamesList();
}
function renderGotyKpi() {
const el = document.getElementById('sglv-goty-kpi');
if (!el) return;
if (!gotyState.loaded) { el.innerHTML = ''; return; }
const total = gotyState.games.length;
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
const owned = gotyState.games.filter(g => isOwned(g.appid)).length;
const notOwned = total - owned;
const srcLabel = gotyState.source || Host.T.gotySourceGithub;
const updatedStr = gotyState.updatedAt
? (Host.isZh ? '更新于 ' : 'Updated ') + gotyState.updatedAt + ' · ' + srcLabel
: (Host.isZh ? '来源:' : 'Source: ') + srcLabel;
el.innerHTML = `
${total.toLocaleString()}
${updatedStr}
${owned.toLocaleString()}
${Host.T.gotyKpiOwnedSub}
${notOwned.toLocaleString()}
${Host.isZh ? '可在 Steam 购买' : 'Available on Steam'}
`;
}
function renderGotyGamesList() {
const content = document.getElementById('sglv-goty-content');
if (!content) return;
_bindGotyCoverDelegation(content); // v1.4.0: 年度大作封面回退委托(捕获阶段,仅绑定一次)
const clearPagination = () => { const p = document.getElementById('sglv-goty-pagination'); if (p) p.innerHTML = ''; };
if (gotyState.loading) {
content.innerHTML = '' + Host.T.gotyLoading + '
';
clearPagination();
return;
}
if (gotyState.loadError) {
content.innerHTML = '' + Host.T.gotyLoadError + '
';
clearPagination();
return;
}
if (!gotyState.loaded || gotyState.games.length === 0) {
content.innerHTML = '' + Host.T.gotyNoData + '
';
clearPagination();
return;
}
const list = getFilteredGoty();
if (list.length === 0) {
content.innerHTML = '' + (Host.isZh ? '无匹配结果' : 'No matching results') + '
';
clearPagination();
return;
}
const { pageItems, totalPages, page: _clampedPage } = Host.paginate(list, gotyState.page, gotyState.pageSize);
gotyState.page = _clampedPage;
const ownedIds = Host.getActiveOwnedAppIds();
const isOwned = n => ownedIds.has(n) || ownedIds.has(String(n));
const gotyBadge = gotyBadgeHtml();
const dateLabel = Host.T.gotyReleaseDate;
// v2.9.22: 发售日期组件——时间图标 + 完整年月日(YYYY-MM-DD),与 owned badge 同行右侧
// 注意:g 是 pageItems.map 回调内的形参,外层不能直接引用,所以用工厂函数
const gotyDateHtml = g => g.date ? '' + Host.ICONS.clock + ' ' + Host.wlEscape(gotyDateFull(g.date)) + ' ' : '';
if (gotyState.viewMode === 'card') {
content.innerHTML = '' + pageItems.map(g => {
const clickUrl = 'https://store.steampowered.com/app/' + g.appid;
return '
' +
'
' + gotyBadge + gotyCoverImgTag(g.appid, g.name, 'poster', 'sglv-card-img') + '
' +
'
' +
'
' + Host.wlEscape(g.name) + '
' +
// 行 2: [是否拥有] [🕐 发售年月日] 紧凑同行
'
' + gotyOwnedBadgeHtml(isOwned(g.appid)) + gotyDateHtml(g) + '
' +
'
' +
'
';
}).join('') + '
';
} else if (gotyState.viewMode === 'cover') {
// v2.9.22: 标签统一规则——封面上不放任何徽章(gotyBadge 在右下角,owned badge + 日期在标题下方同行)
content.innerHTML = '' + pageItems.map(g => {
const clickUrl = 'https://store.steampowered.com/app/' + g.appid;
return '
' +
'
' + gotyBadge + gotyCoverImgTag(g.appid, g.name, 'capsule', 'sglv-wl-cover-img') + '
' +
'
' +
'
' + Host.wlEscape(g.name) + '
' +
// 紧凑:状态 + 日期同行
'
' + gotyOwnedBadgeHtml(isOwned(g.appid)) + gotyDateHtml(g) + '
' +
'
' +
'
';
}).join('') + '
';
} else {
content.innerHTML = '' + pageItems.map(g => {
const clickUrl = 'https://store.steampowered.com/app/' + g.appid;
const iconHtml = gotyCoverImgTag(g.appid, g.name, 'header', 'sglv-list-icon');
return '
' +
iconHtml +
'
' +
'
' + Host.wlEscape(g.name) + ' ' + g.appid + '
' +
'
' + gotyBadge + gotyOwnedBadgeHtml(isOwned(g.appid)) + ' ' + gotyDateHtml(g) + '
' +
'
' +
'
';
}).join('') + '
';
}
const pagination = document.getElementById('sglv-goty-pagination');
if (pagination) {
// v1.9.47-fix: 补 onJump——年度大作翻页 5 个按钮全部走 onJump(参见主脚本 renderPagination)
Host.renderPagination(pagination, 'sglv-goty', gotyState.page, totalPages, list.length,
() => { gotyState.page--; renderGotyGamesList(); },
() => { gotyState.page++; renderGotyGamesList(); },
(p) => { gotyState.page = p; renderGotyGamesList(); });
}
}
// ==================== DLC 收藏标签页 (v1.9.0) ====================
// 数据源: Host.isDlc (双源: Barter.vg DLC DB + Steam type=1/6)
// Host.getDlcParent (DLC→父游戏 appID, 仅 Barter.vg 提供)
// Host.getDlcType (DLC 子类型: music/dlc/video, 需 enrichDlcTypes)
// Host.getBundleCount (进包次数)
// 加载时序: loadDlcDatabase → enrichOwnedAppTypes → enrichDlcTypes (三级渐进)
// 宿主桥接: Host.isDlc, Host.isDlcDbReady, Host.loadDlcDatabase,
// Host.enrichOwnedAppTypes, Host.enrichDlcTypes, Host.getDlcType,
// Host.getBundleCount, Host.getDlcParent, Host.getParentDlcTotalMap
function dlcTypeBadgeHtml(type) {
if (type === 'music') return '' + Host.T.dlcBadgeMusic + ' ';
return 'DLC ';
}
function dlcBundleBadgeHtml(count) {
if (!count || count <= 0) return '';
return '📦' + count + ' ';
}
function dlcPlayedBadgeHtml(playtime) {
if (!playtime || playtime <= 0) return '';
return '' + Math.round(playtime / 60) + 'h ';
}
// v1.9.4: 已拥有/未拥有 徽章
function dlcOwnedBadgeHtml(isOwned) {
if (isOwned) return '' + (Host.isZh ? '已拥有' : 'Owned') + ' ';
return '' + (Host.isZh ? '未拥有' : 'Not Owned') + ' ';
}
function dlcPosterImg(appid, name) {
return Host.posterImg(appid, name);
}
function dlcCapsuleImg(appid, name) {
if (typeof Host.capsuleImg === 'function') return Host.capsuleImg(appid, name);
return Host.posterImg(appid, name);
}
function dlcLookupParentName(parentApp) {
if (!parentApp) return null;
const found = Host.getOwnedGames().find(g => g.appid === parentApp || String(g.appid) === String(parentApp));
return found ? found.name : null;
}
// ==================== v1.9.2: DLC 子类型 IDB 永久缓存(只增不减) ====================
// 首次 enrichDlcTypes 后,所有已 enrich 的 DLC 写入 IDB。
// 下次加载:从 IDB 同步预热 dlcTypeCache,只 enrich 缓存里没有的 DLC("只增不减"增量)。
// 即使主脚本 GM 缓存丢失(用户清理脚本存储),本缓存仍能秒返已知子类型。
async function _ensureDlcTypeCacheLoaded() {
if (dlcState.dlcTypeCacheLoaded) return dlcState.dlcTypeCache;
if (dlcState.idbLoadPromise) return dlcState.idbLoadPromise;
dlcState.idbLoadPromise = (async () => {
if (!_C || !_C.IDB || typeof _C.IDB.loadAll !== 'function') {
console.log('[SGLV-DLC] SGLVCore.IDB 不可用,跳过 IDB 缓存(fallback to in-memory only)');
dlcState.dlcTypeCacheLoaded = true;
return dlcState.dlcTypeCache;
}
try {
await _C.IDB.loadAll();
// 缓存版本号管理:ensureCacheVersions 升级时自动失效旧版本
if (typeof _C.ensureCacheVersions === 'function') {
_C.ensureCacheVersions(_DLC_CACHE_VERSION);
}
const raw = _C.IDB.get(_DLC_TYPE_CACHE_IDB_KEY, null);
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
dlcState.dlcTypeCache = raw;
console.log(`[SGLV-DLC] IDB dlcTypeCache 命中: ${Object.keys(raw).length} 个 DLC 子类型已缓存`);
} else {
console.log('[SGLV-DLC] IDB dlcTypeCache 空,首次使用');
}
} catch (e) {
console.warn('[SGLV-DLC] IDB 加载失败,使用空缓存:', e);
}
dlcState.dlcTypeCacheLoaded = true;
return dlcState.dlcTypeCache;
})();
return dlcState.idbLoadPromise;
}
// 持久化到 IDB(异步写,内存立即可见)
function _persistDlcTypeCache() {
if (!_C || !_C.IDB) return;
try { _C.IDB.set(_DLC_TYPE_CACHE_IDB_KEY, dlcState.dlcTypeCache); } catch (e) { /* quota */ }
}
// 构建已拥有 DLC 列表(v1.9.4: 全量加载所有已知 DLC,匹配 isOwned)
// v1.9.2: 同步版本保留,供"快速终态 rebuild"用;大库存场景请用 buildDlcListAsync
function buildDlcList() {
const t0 = performance.now();
const showFamilyShared = Host.getShowFamilyShared();
const pool = showFamilyShared
? Host.getOwnedGames()
: Host.getOwnedGames().filter(g => Host.isGameOwnedByMe(g));
const ownedMap = {};
pool.forEach(g => { ownedMap[String(g.appid)] = g; });
// v1.9.11: 预构建 parentAppId → name 映射(O(1)查找),替代 dlcLookupParentName 的 O(n) 线性扫描
const parentNameMap = {};
Host.getOwnedGames().forEach(g => { if (g && g.appid) parentNameMap[String(g.appid)] = g.name; });
const dlcsByParent = (typeof Host.getDlcsByParentMap === 'function') ? Host.getDlcsByParentMap() : {};
const allDlcAppIds = new Set();
for (const dlcIds of Object.values(dlcsByParent)) {
for (const id of dlcIds) allDlcAppIds.add(id);
}
for (const g of pool) {
if (Host.isDlc(g.appid)) allDlcAppIds.add(g.appid);
}
console.log(`[SGLV-DLC] buildDlcList — 库存池 ${pool.length} 款 (familyShared=${showFamilyShared}), 全量 DLC ${allDlcAppIds.size} 个, isDlcDbReady=${Host.isDlcDbReady()}`);
dlcState.dlcs = Array.from(allDlcAppIds).map(appid => {
const owned = ownedMap[String(appid)];
const parentApp = Host.getDlcParent(appid);
const type = (dlcState.dlcTypeCache && dlcState.dlcTypeCache[appid]) || Host.getDlcType(appid) || 'dlc';
return {
appid,
name: owned ? owned.name : ('DLC ' + appid),
playtime: owned ? (owned.playtime || 0) : 0,
parentApp: parentApp,
parentName: parentApp ? (parentNameMap[String(parentApp)] || null) : null,
type: type,
bundleCount: Host.getBundleCount(appid),
isOwned: !!owned,
};
});
const withParent = dlcState.dlcs.filter(d => d.parentApp).length;
const withType = dlcState.dlcs.filter(d => d.type !== 'dlc').length;
const withBundle = dlcState.dlcs.filter(d => d.bundleCount > 0).length;
console.log(`[SGLV-DLC] buildDlcList — 完成: ${dlcState.dlcs.length} 个 DLC, 父游戏=${withParent}, 子类型=${withType}, 进包=${withBundle}, 耗时 ${Math.round(performance.now() - t0)}ms`);
}
// v1.9.2: buildDlcList 的 async 分片版本——每 300 个 yield 一次(setTimeout 0 让出主线程)
// 大库存(2k+)场景避免单次同步 200-500ms 卡顿;每个 chunk 后用 setTimeout 让浏览器处理用户输入
// v1.9.4: 全量加载——遍历所有已拥有父游戏的全部 DLC(含未拥有),匹配 isOwned
async function buildDlcListAsync() {
const t0 = performance.now();
const showFamilyShared = Host.getShowFamilyShared();
const allOwned = Host.getOwnedGames();
const pool = showFamilyShared ? allOwned : allOwned.filter(g => Host.isGameOwnedByMe(g));
// v1.9.4: 构建 appid → ownedGame 映射,用于匹配已拥有 DLC 和父游戏名称
const ownedMap = {};
pool.forEach(g => { ownedMap[String(g.appid)] = g; });
// v1.9.11: 预构建 parentAppId → name 映射(O(1)查找),替代 dlcLookupParentName 的 O(n) 线性扫描
// 71371 DLC × 10891 games 线性扫描 = ~7.8亿次比较,导致 buildDlcListAsync 永远无法完成
const parentNameMap = {};
allOwned.forEach(g => { if (g && g.appid) parentNameMap[String(g.appid)] = g.name; });
// v1.9.4: 获取父游戏→DLC列表 反向映射(Host.getDlcsByParentMap)
const dlcsByParent = (typeof Host.getDlcsByParentMap === 'function') ? Host.getDlcsByParentMap() : {};
const parentKeys = Object.keys(dlcsByParent);
// 收集所有需要处理的 DLC appid(全量 = 所有已知 DLC)
const allDlcAppIds = new Set();
// 从父游戏反向映射收集
for (const dlcIds of Object.values(dlcsByParent)) {
for (const id of dlcIds) allDlcAppIds.add(id);
}
// 同时保留原有逻辑:已拥有且 isDlc 的也加入(可能有些 DLC 不在 Barter.vg 数据库)
for (const g of pool) {
if (Host.isDlc(g.appid)) allDlcAppIds.add(g.appid);
}
const dlcCandidates = Array.from(allDlcAppIds);
console.log(`[SGLV-DLC] buildDlcListAsync — 库存池 ${pool.length} 款, 父游戏 ${parentKeys.length} 个, 全量 DLC ${dlcCandidates.length} 个 (含未拥有)`);
const out = [];
const CHUNK = 300;
for (let i = 0; i < dlcCandidates.length; i += CHUNK) {
const slice = dlcCandidates.slice(i, i + CHUNK);
slice.forEach(appid => {
const owned = ownedMap[String(appid)];
const parentApp = Host.getDlcParent(appid);
let type = (dlcState.dlcTypeCache && dlcState.dlcTypeCache[appid]) || Host.getDlcType(appid) || 'dlc';
out.push({
appid,
name: owned ? owned.name : ('DLC ' + appid),
playtime: owned ? (owned.playtime || 0) : 0,
parentApp,
parentName: parentApp ? (parentNameMap[String(parentApp)] || null) : null,
type,
bundleCount: Host.getBundleCount(appid),
isOwned: !!owned,
});
});
// 每片后让出主线程——浏览器可处理 UI 事件(点击、滚动等)
if (i + CHUNK < dlcCandidates.length) {
await new Promise(r => setTimeout(r, 0));
}
}
dlcState.dlcs = out;
// v1.9.8: 清除过滤缓存(dlcs 内容已变,需重算 filtered/sorted 结果)
dlcState._filteredCache = null;
dlcState._filteredCacheKey = null;
const withParent = out.filter(d => d.parentApp).length;
const withType = out.filter(d => d.type !== 'dlc').length;
const withBundle = out.filter(d => d.bundleCount > 0).length;
console.log(`[SGLV-DLC] buildDlcListAsync — 完成: ${out.length} 个 DLC, 父游戏=${withParent}, 子类型=${withType}, 进包=${withBundle}, 耗时 ${Math.round(performance.now() - t0)}ms`);
}
// 三级渐进加载: Barter.vg DLC DB → Steam 全库存 type → DLC 子类型
// v1.9.1: L1 + L2 并行启动,任一完成立即 schedule buildDlcList + render(不阻塞 UI 进程)
// v1.9.2: 四阶段加载——阶段 0 同步预热(IDB + 主脚本闭包),秒出;无缓存才走阶段 1+2+3 冷启
// L3 真正"只增不减"增量:只 enrich dlcTypeCache 里没有的 DLC
// v1.9.8: 渐进式渲染——L1+L2 完成后立即 loaded=true 渲染数据,L3 后台异步获取子类型
// DLC 数据只增不减,缓存永久有效,支持增量更新;修复加载完成但一直显示加载中的问题
async function loadDlcData(force) {
if (dlcState.loading) {
// v1.9.27: force 时重置可能卡死的 loading 标志
if (!force) {
console.log('[SGLV-DLC] loadDlcData — 已在加载中, 跳过');
return;
}
console.log('[SGLV-DLC] loadDlcData — force 重置卡死的 loading 标志');
dlcState.loading = false;
}
if (dlcState.loaded && !force) {
console.log('[SGLV-DLC] loadDlcData — 已加载且非强制, 直接渲染');
renderDlcSections();
return;
}
const t0 = performance.now();
console.log(`[SGLV-DLC] loadDlcData — 开始加载 (force=${!!force}, dlcState.loaded=${dlcState.loaded})`);
dlcState.loading = true;
dlcState.loadError = null;
dlcState.stage = 0;
dlcState.level1Done = false;
dlcState.level2Done = false;
dlcState.rebuildQueued = false;
// ==================== 阶段 0: 同步预热(关键!) ====================
// 1) IDB 永久缓存 → 立即填 dlcTypeCache(只增不减,即使主脚本 GM 缓存丢失也能秒返)
// 2) Host.loadDlcDatabaseFromCacheSync → 同步填主脚本 dlcDbData / appTypeMap 闭包
// 3) buildDlcListAsync → 立即 build(分片,非阻塞)
// 4) 如果拿到数据(dlcState.dlcs.length > 0),直接 loaded=true → 秒出 UI
// 5) 后台异步跑 L1+L2(刷新可能新增的 DLC)+ L3 增量 enrich
try {
await _ensureDlcTypeCacheLoaded();
if (typeof Host.loadDlcDatabaseFromCacheSync === 'function') {
try { Host.loadDlcDatabaseFromCacheSync(); } catch (e) { /* noop */ }
}
await buildDlcListAsync();
if (dlcState.dlcs.length > 0) {
dlcState.loaded = true;
dlcState.loading = false;
dlcState.stage = 0;
try { dlcState.parentTotalMap = Host.getParentDlcTotalMap() || {}; } catch (e) { dlcState.parentTotalMap = {}; }
renderDlcSections();
console.log(`[SGLV-DLC] 阶段 0 缓存命中,立即出 ${dlcState.dlcs.length} 个 DLC,耗时 ${Math.round(performance.now() - t0)}ms`);
// 后台异步刷新(不阻塞,fire-and-forget)
_backgroundDlcRefresh();
return;
}
console.log('[SGLV-DLC] 阶段 0 无缓存(dlcs=0),降级到阶段 1+2+3 冷启');
} catch (e) {
console.warn('[SGLV-DLC] 阶段 0 预热失败,降级到冷启:', e);
}
// ==================== 阶段 1+2: 冷启(无缓存场景) ====================
dlcState.stage = 1;
renderDlcSections();
if (_StageProgress && typeof _StageProgress.ensure === 'function') {
try { _StageProgress.ensure(); _StageProgress.setStage(1, 3, 'DLC'); } catch (e) { /* noop */ }
}
// ---- 工具:scheduleRebuild ----
const scheduleRebuild = (reason) => {
if (dlcState.rebuildQueued) return;
dlcState.rebuildQueued = true;
setTimeout(() => {
dlcState.rebuildQueued = false;
if (_collectionActiveTab !== 'dlc') return;
(async () => {
try {
const t = performance.now();
await buildDlcListAsync();
renderDlcSections();
console.log(`[SGLV-DLC] scheduleRebuild(${reason}) — dlcs=${dlcState.dlcs.length}, 渲染耗时 ${Math.round(performance.now() - t)}ms`);
} catch (e) {
console.warn('[SGLV-DLC] scheduleRebuild failed:', e);
}
})();
}, 0);
};
try {
// ==================== Level 1 ∥ Level 2 并行 ====================
const p1 = (async () => {
const t1 = performance.now();
try {
await Host.loadDlcDatabase();
dlcState.level1Done = true;
console.log(`[SGLV-DLC] [Level 1/3] Barter.vg 就绪, 耗时 ${Math.round(performance.now() - t1)}ms`);
scheduleRebuild('L1-done');
} catch (e) {
console.warn('[SGLV-DLC] [Level 1/3] 失败:', e);
}
})();
const p2 = (async () => {
const t2 = performance.now();
try {
await Host.enrichOwnedAppTypes();
dlcState.level2Done = true;
dlcState.stage = 2;
if (_StageProgress) try { _StageProgress.setStage(2, 3, 'appType'); } catch (e) {}
console.log(`[SGLV-DLC] [Level 2/3] appType 就绪, 耗时 ${Math.round(performance.now() - t2)}ms`);
scheduleRebuild('L2-done');
} catch (e) {
console.warn('[SGLV-DLC] [Level 2/3] 失败:', e);
}
})();
await Promise.all([p1, p2]);
// v1.9.8: L1+L2 完成后立即 build + 渐进式渲染(loaded=true),L3 后台异步
dlcState.stage = 2;
await buildDlcListAsync();
try { dlcState.parentTotalMap = Host.getParentDlcTotalMap() || {}; } catch (e) { dlcState.parentTotalMap = {}; }
// v1.9.8: 关键改变 — L1+L2 完成后立即标记 loaded=true,用户可以看到 DLC 列表
// L3 (DLC 子类型) 在后台异步获取,不阻塞 UI
dlcState.loaded = true;
dlcState.loading = false;
dlcState.page = 1;
dlcState.stage = 0;
if (_StageProgress) try { _StageProgress.hide(); } catch (e) {}
renderDlcSections();
console.log(`[SGLV-DLC] L1+L2 完成, 渐进式渲染 ${dlcState.dlcs.length} 个 DLC, 耗时 ${Math.round(performance.now() - t0)}ms`);
// v1.9.8: L3 后台异步获取 DLC 子类型(fire-and-forget,不阻塞 UI)
_backgroundDlcTypeEnrich();
} catch (e) {
dlcState.loadError = e.message || 'Load failed';
console.warn(`[SGLV-DLC] loadDlcData — 加载失败: ${e.message}, 已耗时 ${Math.round(performance.now() - t0)}ms`, e);
dlcState.loading = false;
if (_StageProgress) try { _StageProgress.hide(); } catch (e) {}
renderDlcSections();
}
}
// v1.9.8: 后台 DLC 子类型增量获取 — L1+L2 完成后 fire-and-forget
// 从 dlcTypeCache 中筛选未获取类型的 DLC,调用 Host.enrichDlcTypes 增量获取
// 获取完成后 rebuild + re-render,更新 KPI 和卡片类型显示
// DLC 数据只增不减,dlcTypeCache 永久缓存(IDB),支持增量更新
async function _backgroundDlcTypeEnrich() {
if (dlcState.dlcTypeEnriching) {
console.log('[SGLV-DLC] [bg L3] 已在获取中, 跳过');
return;
}
const dlcIds = dlcState.dlcs.map(d => d.appid);
if (dlcIds.length === 0) return;
// 增量:只获取 dlcTypeCache 中没有的 DLC
const dlcIdsToEnrich = dlcIds.filter(id => !dlcState.dlcTypeCache[id]);
if (dlcIdsToEnrich.length === 0) {
console.log(`[SGLV-DLC] [bg L3] 跳过: 全部 ${dlcIds.length} 个 DLC 已在本地 IDB 缓存中`);
return;
}
dlcState.dlcTypeEnriching = true;
const t3 = performance.now();
console.log(`[SGLV-DLC] [bg L3] 增量 enrich ${dlcIdsToEnrich.length}/${dlcIds.length} 个 DLC(命中 ${dlcIds.length - dlcIdsToEnrich.length})`);
try {
// v2.9.68: 使用 onProgress 回调实现渐进式 re-render
let renderTimer = null;
await Host.enrichDlcTypes(dlcIdsToEnrich, {
onProgress: (done, total) => {
// 节流渲染:每 1.5s 最多刷新一次 UI
if (!renderTimer) {
renderTimer = setTimeout(() => {
renderTimer = null;
if (_collectionActiveTab !== 'dlc') return;
(async () => {
try {
// 从主脚本拉取已获取的类型,合并到 dlcTypeCache
let newCount = 0;
for (const id of dlcIdsToEnrich) {
const t = Host.getDlcType(id);
if (t && !dlcState.dlcTypeCache[id]) {
dlcState.dlcTypeCache[id] = t;
newCount++;
}
}
if (newCount > 0) {
_persistDlcTypeCache();
// 清除过滤缓存,让新类型生效
dlcState._filteredCache = null;
dlcState._filteredCacheKey = null;
await buildDlcListAsync();
renderDlcSections();
}
} catch (e) { /* noop */ }
})();
}, 1500);
}
}
});
// 最终:拉取所有类型,rebuild + re-render
let newCount = 0;
for (const id of dlcIdsToEnrich) {
const t = Host.getDlcType(id);
if (t && !dlcState.dlcTypeCache[id]) {
dlcState.dlcTypeCache[id] = t;
newCount++;
}
}
if (newCount > 0) {
_persistDlcTypeCache();
dlcState._filteredCache = null;
dlcState._filteredCacheKey = null;
await buildDlcListAsync();
}
try { dlcState.parentTotalMap = Host.getParentDlcTotalMap() || {}; } catch (e) {}
if (_collectionActiveTab === 'dlc') renderDlcSections();
console.log(`[SGLV-DLC] [bg L3] 增量完成: 新增 ${newCount} 个 type, 累计 ${Object.keys(dlcState.dlcTypeCache).length} 个, 耗时 ${Math.round(performance.now() - t3)}ms`);
} catch (e) {
console.warn('[SGLV-DLC] [bg L3] 失败:', e);
} finally {
dlcState.dlcTypeEnriching = false;
}
}
// v1.9.2: 后台异步刷新——阶段 0 缓存命中后,fire-and-forget 跑 L1+L2 + L3 增量
// v1.9.3: 节流(6h)——避免短时间内重复刷新
// 目标:在不阻塞 UI 的前提下,让 dlcTypeCache 持续累计(只增不减,后续秒返)
async function _backgroundDlcRefresh() {
// v1.9.3: 节流——6h 内已刷过则跳过(多次打开 DLC tab 不会重复跑)
if (Date.now() - (dlcState.lastBgRefreshAt || 0) < _DLC_BG_REFRESH_INTERVAL) {
console.log(`[SGLV-DLC] 后台刷新跳过(节流): 上次 ${new Date(dlcState.lastBgRefreshAt).toLocaleString()}`);
return;
}
dlcState.lastBgRefreshAt = Date.now();
const tb = performance.now();
try {
// L1 ∥ L2 并行(主脚本缓存命中时秒返,否则走网络)
const p1 = Host.loadDlcDatabase()
.then(() => { dlcState.level1Done = true; })
.catch(e => console.warn('[SGLV-DLC] [bg L1] 失败:', e));
const p2 = Host.enrichOwnedAppTypes()
.then(() => { dlcState.level2Done = true; })
.catch(e => console.warn('[SGLV-DLC] [bg L2] 失败:', e));
await Promise.all([p1, p2]);
console.log(`[SGLV-DLC] [bg] L1+L2 完成, 耗时 ${Math.round(performance.now() - tb)}ms`);
// 重建(可能新增 DLC)
await buildDlcListAsync();
dlcState._filteredCache = null;
dlcState._filteredCacheKey = null;
if (_collectionActiveTab === 'dlc') renderDlcSections();
// v1.9.8: L3 增量 enrich 委托给 _backgroundDlcTypeEnrich(复用渐进式渲染逻辑)
await _backgroundDlcTypeEnrich();
console.log(`[SGLV-DLC] 后台刷新完成, 总耗时 ${Math.round(performance.now() - tb)}ms`);
} catch (e) {
console.warn('[SGLV-DLC] 后台刷新失败:', e);
}
}
// 筛选 + 排序后的扁平 DLC 列表
// v1.9.8: 缓存结果——当 dlcs/search/typeFilter/ownFilter/sort 未变时跳过重算,减少点击卡顿
function getFilteredDlc() {
const cacheKey = `${dlcState.dlcs.length}|${dlcState.search}|${dlcState.typeFilter}|${dlcState.ownFilter}|${dlcState.sort}`;
if (dlcState._filteredCacheKey === cacheKey && dlcState._filteredCache) {
return dlcState._filteredCache;
}
const q = dlcState.search.toLowerCase().trim();
let list = dlcState.dlcs.slice();
if (q) {
list = list.filter(d => {
if (String(d.appid).includes(q)) return true;
if ((d.name || '').toLowerCase().includes(q)) return true;
if (d.parentName && d.parentName.toLowerCase().includes(q)) return true;
return false;
});
}
if (dlcState.typeFilter === 'dlc') list = list.filter(d => d.type === 'dlc');
else if (dlcState.typeFilter === 'music') list = list.filter(d => d.type === 'music');
else if (dlcState.typeFilter === 'bundled') list = list.filter(d => d.bundleCount > 0);
else if (dlcState.typeFilter === 'played') list = list.filter(d => d.playtime > 0);
// v1.9.4: 已拥有筛选
if (dlcState.ownFilter === 'owned') list = list.filter(d => d.isOwned);
// v1.9.5: 使用缓存的 _nameCollator,避免每次渲染重新构造
// v1.9.13: 激进防御——sort 出错时降级为 appid 排序,绝不抛异常中断渲染
try {
list.sort((a, b) => {
let r = 0;
switch (dlcState.sort) {
case 'appidAsc': r = a.appid - b.appid; break;
case 'appidDesc': r = b.appid - a.appid; break;
case 'nameAsc': r = _nameCollator.compare(a.name || '~', b.name || '~') || (a.appid - b.appid); break;
case 'typeAsc': r = (a.type || 'z').localeCompare(b.type || 'z') || (a.appid - b.appid); break;
case 'parentAsc':
default:
r = _nameCollator.compare(a.parentName || a.parentApp || '~', b.parentName || b.parentApp || '~') || (a.appid - b.appid);
}
return r || (a.appid - b.appid);
});
} catch (sortErr) {
console.warn('[SGLV-DLC] getFilteredDlc sort 失败,降级 appid 排序:', sortErr);
list.sort((a, b) => a.appid - b.appid);
}
// v1.9.8: 缓存结果
dlcState._filteredCache = list;
dlcState._filteredCacheKey = cacheKey;
return list;
}
// 按父游戏分组(用于分组视图)
function getGroupedDlc(filtered) {
const groups = {};
const noParent = [];
filtered.forEach(d => {
if (d.parentApp) {
const key = String(d.parentApp);
if (!groups[key]) groups[key] = { parentApp: d.parentApp, parentName: d.parentName, dlcs: [] };
groups[key].dlcs.push(d);
} else {
noParent.push(d);
}
});
let groupList = Object.values(groups);
// v1.9.5: 使用缓存的 _nameCollator
// v1.9.13: 激进防御——sort 出错时降级为不排序
try {
groupList.sort((a, b) => _nameCollator.compare(a.parentName || String(a.parentApp), b.parentName || String(b.parentApp)));
} catch (sortErr) {
console.warn('[SGLV-DLC] getGroupedDlc sort 失败,跳过排序:', sortErr);
}
if (noParent.length > 0) {
groupList.push({ parentApp: null, parentName: Host.T.dlcUnknownParent, dlcs: noParent });
}
// 计算完成度(v1.9.4: ownedDlc 只统计已拥有,totalDlc 为全部已知 DLC)
groupList.forEach(g => {
g.totalDlc = g.dlcs.length;
g.ownedDlc = g.dlcs.filter(d => d.isOwned).length;
g.pct = g.totalDlc > 0 ? Math.round(g.ownedDlc / g.totalDlc * 100) : 100;
});
return groupList;
}
function renderDlcTab(parent) {
const wrap = document.createElement('div');
wrap.className = 'sglv-psplus-wrap';
const kpi = document.createElement('div');
kpi.id = 'sglv-dlc-kpi';
wrap.appendChild(kpi);
const toolbar = document.createElement('div');
toolbar.className = 'sglv-toolbar';
const sortOptions = [
{ key: 'parentAsc', label: Host.T.dlcSortParent },
{ key: 'appidDesc', label: Host.T.dlcSortAppidDesc },
{ key: 'appidAsc', label: Host.T.dlcSortAppidAsc },
{ key: 'nameAsc', label: Host.T.dlcSortNameAsc },
{ key: 'typeAsc', label: Host.T.dlcSortType },
];
// v1.9.1: groupMode=true 时视图按钮置灰(分组视图天然是 card 布局,viewMode 概念对它无意义)
const viewToggleDisabledCls = dlcState.groupMode ? ' sglv-view-disabled' : '';
toolbar.innerHTML = `
${sortOptions.map(o => `${o.label} `).join('')}
${Host.ICONS.grid}
${Host.ICONS.cover}
${Host.ICONS.list}
${Host.isZh ? '全部' : 'All'}
${Host.isZh ? '已拥有' : 'Owned'}
${Host.T.dlcGroupByParent}
${Host.T.dlcFlatList}
`;
wrap.appendChild(toolbar);
const content = document.createElement('div');
content.className = 'sglv-content';
content.id = 'sglv-dlc-content';
wrap.appendChild(content);
parent.appendChild(wrap);
// 事件绑定
toolbar.querySelector('#sglv-dlc-search').addEventListener('input', _debounce((e) => {
dlcState.search = e.target.value;
dlcState.page = 1;
renderDlcGamesList();
}, 200));
toolbar.querySelector('#sglv-dlc-sort').addEventListener('change', (e) => {
dlcState.sort = e.target.value;
dlcState.page = 1;
renderDlcGamesList();
});
const setDlcViewMode = (mode) => {
// v1.9.1: groupMode=true 时视图按钮被禁用,直接返回(双保险,防止 pointer-events bypass)
if (dlcState.groupMode) return;
dlcState.viewMode = mode;
['card', 'cover', 'list'].forEach(m => {
toolbar.querySelector('#sglv-dlc-' + m)?.classList.toggle('active', m === mode);
});
renderDlcGamesList();
};
toolbar.querySelector('#sglv-dlc-card').addEventListener('click', () => setDlcViewMode('card'));
toolbar.querySelector('#sglv-dlc-cover').addEventListener('click', () => setDlcViewMode('cover'));
toolbar.querySelector('#sglv-dlc-list').addEventListener('click', () => setDlcViewMode('list'));
// v1.9.4: 已拥有筛选切换
const setDlcOwnFilter = (filter) => {
dlcState.ownFilter = filter;
dlcState.page = 1;
toolbar.querySelector('#sglv-dlc-own-all')?.classList.toggle('active', filter === 'all');
toolbar.querySelector('#sglv-dlc-own-owned')?.classList.toggle('active', filter === 'owned');
renderDlcGamesList();
};
toolbar.querySelector('#sglv-dlc-own-all').addEventListener('click', () => setDlcOwnFilter('all'));
toolbar.querySelector('#sglv-dlc-own-owned').addEventListener('click', () => setDlcOwnFilter('owned'));
// v1.9.1: 统一 grouped/flat 切换逻辑,同步视图按钮置灰状态
const applyDlcGroupMode = (grouped) => {
dlcState.groupMode = grouped;
dlcState.page = 1;
toolbar.querySelector('#sglv-dlc-grouped').classList.toggle('active', grouped);
toolbar.querySelector('#sglv-dlc-flat').classList.toggle('active', !grouped);
toolbar.querySelector('.sglv-view-toggle')?.classList.toggle('sglv-view-disabled', grouped);
renderDlcGamesList();
};
toolbar.querySelector('#sglv-dlc-grouped').addEventListener('click', () => applyDlcGroupMode(true));
toolbar.querySelector('#sglv-dlc-flat').addEventListener('click', () => applyDlcGroupMode(false));
// KPI 卡片点击筛选
kpi.addEventListener('click', e => {
const card = e.target.closest('[data-dlc-filter]');
if (!card) return;
const f = card.dataset.dlcFilter;
dlcState.typeFilter = dlcState.typeFilter === f ? 'all' : f;
dlcState.page = 1;
renderDlcKpi();
renderDlcGamesList();
});
// v1.9.3: 用户点击 tab 立即进入——先无条件渲染骨架(KPI + content 都是 skeleton)
// 后台 loadDlcData 完成后再 renderDlcSections() 替换为真实数据
// 这样无论冷启/热数据都能"秒进"
renderDlcSections();
if (!dlcState.loaded && !dlcState.loading) {
// fire-and-forget:后台加载,不阻塞 UI
loadDlcData();
}
}
function renderDlcSections() {
if (_collectionActiveTab !== 'dlc' || !document.getElementById('sglv-dlc-content')) return;
// v1.9.13: 激进防御——任何渲染异常都不应中断 loadDlcData 流程
try {
renderDlcKpi();
renderDlcGamesList();
} catch (e) {
console.warn('[SGLV-DLC] renderDlcSections 渲染异常(已降级):', e);
const content = document.getElementById('sglv-dlc-content');
if (content && !content.innerHTML.trim()) {
content.innerHTML = '' + (Host.isZh ? 'DLC 渲染异常,数据已加载,请尝试切换排序或刷新页面' : 'DLC render error, data loaded. Try switching sort or refresh') + '
';
}
}
}
// v1.9.1: 阶段进度条 + DLC 卡片骨架 HTML 工厂
// 用于 loading 状态:KPI 区 5 个 shimmer 卡 + content 区阶段条 + 12 个 card 占位
// 数据稳定(loaded=true)前不展示真实 KPI 数字,避免数字跳变
// v1.9.4: spinner 加载等待动画(参考 steam-friend-manager 的 spinner 风格)
// v2.9.67: 修复 wlEscape 三元表达式 bug(两分支都调用了 Host.wlEscape)
function renderDlcLoadingHtml() {
const stage = DLC_STAGES[dlcState.stage] || { pct: 30, zh: '加载中…', en: 'Loading…' };
const pct = stage.pct;
const text = Host && Host.isZh ? stage.zh : stage.en;
const safeText = Host && Host.wlEscape ? Host.wlEscape(text) : text;
const spinSvg = ' ';
return `
${spinSvg}
${safeText}
${Host && Host.isZh ? '正在加载所有已知 DLC 数据…' : 'Loading all known DLC data…'}
`;
}
function renderDlcKpiSkeletonHtml() {
const skel = '
';
const cards = Array.from({ length: 5 }, () => `${skel}
`).join('');
return `${cards}
`;
}
function renderDlcKpi() {
const el = document.getElementById('sglv-dlc-kpi');
if (!el) return;
// v1.9.1: 加载中 / 未加载完成统一显示骨架(数字会跳变,体验差)
if (dlcState.loading || !dlcState.loaded) {
el.innerHTML = renderDlcKpiSkeletonHtml();
return;
}
const total = dlcState.dlcs.length;
const owned = dlcState.dlcs.filter(d => d.isOwned).length;
const regular = dlcState.dlcs.filter(d => d.type === 'dlc').length;
const music = dlcState.dlcs.filter(d => d.type === 'music').length;
const bundled = dlcState.dlcs.filter(d => d.bundleCount > 0).length;
const played = dlcState.dlcs.filter(d => d.playtime > 0).length;
const parentCount = new Set(dlcState.dlcs.filter(d => d.parentApp).map(d => String(d.parentApp))).size;
const noParent = dlcState.dlcs.filter(d => !d.parentApp).length;
// v1.9.8: 后台子类型获取中时,在 KPI 区底部显示进度提示
const enrichHint = dlcState.dlcTypeEnriching
? `
${Host.isZh ? '后台获取 DLC 子类型中…' : 'Fetching DLC subtypes in background…'}
`
: '';
const ownPct = total > 0 ? Math.round(owned / total * 100) : 0;
const subText = Host.T.dlcKpiSub.replace('{n}', parentCount) + (noParent > 0 ? ' · ' + Host.T.dlcKpiNoParent + ' ' + noParent : '');
el.innerHTML = `
${total.toLocaleString()}
${subText}
${Host.isZh ? '已拥有 ' + owned.toLocaleString() + ' (' + ownPct + '%)' : 'Owned ' + owned.toLocaleString() + ' (' + ownPct + '%)'}
${regular.toLocaleString()}
type=1
${music.toLocaleString()}
type=6
${bundled.toLocaleString()}
${Host.isZh ? '含 Bundle' : 'In Bundles'}
${played.toLocaleString()}
playtime>0
${enrichHint}
`;
}
// v1.9.1: 大网格 rAF 分片 append,避免一次性 innerHTML 写入上千卡片卡顿
// 用 DocumentFragment 多次 append,rAF 让出浏览器绘制
const _DLC_RENDER_CHUNK = 24; // 每片卡片数
const _DLC_RENDER_RAF_THRESHOLD = 30; // 超过此数才走分片(少量直接 innerHTML 更快)
function appendDlcCardsChunked(grid, htmls, done) {
if (!grid || !htmls || htmls.length === 0) { if (done) done(); return; }
if (htmls.length <= _DLC_RENDER_RAF_THRESHOLD || typeof requestAnimationFrame !== 'function') {
grid.innerHTML = htmls.join('');
if (done) done();
return;
}
let i = 0;
const total = htmls.length;
const step = () => {
const frag = document.createDocumentFragment();
const t0 = performance.now();
const end = Math.min(i + _DLC_RENDER_CHUNK, total);
for (; i < end; i++) {
const tpl = document.createElement('template');
tpl.innerHTML = htmls[i].trim();
const node = tpl.content.firstChild;
if (node) frag.appendChild(node);
}
grid.appendChild(frag);
if (i < total) {
// 切片之间让出主线程:rAF 等同 16ms,若耗时 > 12ms 也强制让出
if (performance.now() - t0 > 12) {
setTimeout(step, 0);
} else {
requestAnimationFrame(step);
}
} else {
if (done) done();
}
};
step();
}
function renderDlcGamesList() {
const content = document.getElementById('sglv-dlc-content');
if (!content) return;
const clearPagination = () => { const p = document.getElementById('sglv-dlc-pagination'); if (p) p.innerHTML = ''; };
// v1.9.4: loading 阶段显示 spinner 加载动画(参考 steam-friend-manager 风格)
// v2.9.67: 尚未加载(!loaded && !loading)时也显示加载动画,避免首次进入空白
if (dlcState.loading || (!dlcState.loaded && !dlcState.loadError)) {
content.innerHTML = renderDlcLoadingHtml();
clearPagination();
return;
}
if (dlcState.loadError) {
content.innerHTML = '' + Host.T.dlcLoadError + '
';
clearPagination();
return;
}
if (!dlcState.loaded || dlcState.dlcs.length === 0) {
content.innerHTML = '' + Host.T.dlcNoData + '
';
clearPagination();
return;
}
// v1.9.13: 激进防御——getFilteredDlc 出错时降级为未排序原始列表,绝不白屏
let list;
try {
list = getFilteredDlc();
} catch (e) {
console.warn('[SGLV-DLC] getFilteredDlc 异常,降级未排序列表:', e);
list = dlcState.dlcs.slice();
}
if (list.length === 0) {
content.innerHTML = '' + (Host.isZh ? '无匹配结果' : 'No matching results') + '
';
clearPagination();
return;
}
// 分组视图
if (dlcState.groupMode) {
let groups;
try {
groups = getGroupedDlc(list);
} catch (e) {
console.warn('[SGLV-DLC] getGroupedDlc 异常,降级扁平视图:', e);
dlcState.groupMode = false;
groups = null;
}
if (groups) {
const { pageItems, totalPages, page: _clampedPage } = Host.paginate(groups, dlcState.page, dlcState.pageSize);
dlcState.page = _clampedPage;
renderDlcGrouped(content, pageItems);
const pagination = document.getElementById('sglv-dlc-pagination');
if (pagination) {
// v1.9.47-fix: 补 onJump——DLC 翻页 5 个按钮全部走 onJump
Host.renderPagination(pagination, 'sglv-dlc', dlcState.page, totalPages, groups.length,
() => { dlcState.page--; renderDlcGamesList(); },
() => { dlcState.page++; renderDlcGamesList(); },
(p) => { dlcState.page = p; renderDlcGamesList(); });
}
return;
}
}
// 扁平视图
const { pageItems, totalPages, page: _clampedPage } = Host.paginate(list, dlcState.page, dlcState.pageSize);
dlcState.page = _clampedPage;
// v1.9.1: 先建空 grid 容器,再 rAF 分片 append(避免上千卡片 innerHTML 解析卡顿)
let gridClass = 'sglv-card-grid';
let cardHtmls;
if (dlcState.viewMode === 'card') {
gridClass = 'sglv-card-grid';
cardHtmls = pageItems.map(d => {
const clickUrl = 'https://store.steampowered.com/app/' + d.appid;
const badges = dlcOwnedBadgeHtml(d.isOwned) + dlcTypeBadgeHtml(d.type) + dlcBundleBadgeHtml(d.bundleCount) + dlcPlayedBadgeHtml(d.playtime);
const dimCls = d.isOwned ? '' : ' sglv-dlc-not-owned';
return '' +
'
' + dlcPosterImg(d.appid, d.name) + '
' +
'
' +
'
' + Host.wlEscape(d.name) + '
' +
'
AppID: ' + d.appid + '
' +
'
' + badges + '
' +
(d.parentName ? '
' + Host.wlEscape(d.parentName) + '
' : '') +
'
' +
'
';
});
} else if (dlcState.viewMode === 'cover') {
gridClass = 'sglv-wl-cover-grid';
cardHtmls = pageItems.map(d => {
const clickUrl = 'https://store.steampowered.com/app/' + d.appid;
const badges = dlcOwnedBadgeHtml(d.isOwned) + dlcTypeBadgeHtml(d.type) + dlcBundleBadgeHtml(d.bundleCount) + dlcPlayedBadgeHtml(d.playtime);
const dimCls = d.isOwned ? '' : ' sglv-dlc-not-owned';
return '' +
'
' + dlcCapsuleImg(d.appid, d.name) + '
' +
'
' +
'
' + Host.wlEscape(d.name) + ' ' + d.appid + '
' +
'
' + badges + '
' +
'
' +
'
';
});
} else {
gridClass = 'sglv-list-view';
cardHtmls = pageItems.map(d => {
const clickUrl = 'https://store.steampowered.com/app/' + d.appid;
const iconHtml = ' ';
const badges = dlcOwnedBadgeHtml(d.isOwned) + dlcTypeBadgeHtml(d.type) + dlcBundleBadgeHtml(d.bundleCount) + dlcPlayedBadgeHtml(d.playtime);
const dimCls = d.isOwned ? '' : ' sglv-dlc-not-owned';
return '' +
iconHtml +
'
' +
'
' + Host.wlEscape(d.name) + ' ' + d.appid + '
' +
'
' + badges + (d.parentName ? ' · ' + Host.wlEscape(d.parentName) : '') + '
' +
'
' +
'
';
});
}
content.innerHTML = `
`;
const grid = document.getElementById('sglv-dlc-grid');
appendDlcCardsChunked(grid, cardHtmls);
const pagination = document.getElementById('sglv-dlc-pagination');
if (pagination) {
// v1.9.47-fix: 补 onJump——DLC 翻页 5 个按钮全部走 onJump
Host.renderPagination(pagination, 'sglv-dlc', dlcState.page, totalPages, list.length,
() => { dlcState.page--; renderDlcGamesList(); },
() => { dlcState.page++; renderDlcGamesList(); },
(p) => { dlcState.page = p; renderDlcGamesList(); });
}
}
// 分组视图渲染:父游戏折叠头 + DLC 卡片子网格
// v1.9.1: group 头一次性 innerHTML 写入(数量小),展开的 group 内卡片用 rAF 分片 append
// 解决"几百个父游戏全展开 → 几千卡片 innerHTML 解析"卡死
function renderDlcGrouped(content, groups) {
// 1. 先构造所有 group 的外层骨架(头 + 空 card-row 占位),用 data-group-idx 关联
const groupHtmls = groups.map((g, gi) => {
const expanded = dlcState.expandedParents.has(String(g.parentApp));
const arrow = expanded ? '▼' : '▶';
return '' +
'
' +
'' + arrow + ' ' +
'' + Host.wlEscape(g.parentName || String(g.parentApp)) + ' ' +
'' + g.ownedDlc + '/' + g.totalDlc + ' DLC ' +
'' + g.pct + '% ' +
'
' +
(expanded ? '
' : '') +
'
';
});
content.innerHTML = groupHtmls.join('');
// 2. 绑定折叠/展开
content.querySelectorAll('.sglv-dlc-group-head').forEach(head => {
head.addEventListener('click', () => {
const p = head.dataset.parent;
if (dlcState.expandedParents.has(p)) dlcState.expandedParents.delete(p);
else dlcState.expandedParents.add(p);
renderDlcGamesList();
});
});
// 3. 展开的 group:把 DLC 卡片 HTML 准备好,分片 append 到对应 card-row
// 各 group 独立分片,互不阻塞(DOM 解析局部化)
// v1.9.3: 横版封面布局,每行 4 列(替代原 8 列窄图)
groups.forEach((g, gi) => {
if (!dlcState.expandedParents.has(String(g.parentApp))) return;
if (!g.dlcs || g.dlcs.length === 0) return;
const cardHtmls = g.dlcs.map(d => {
const clickUrl = 'https://store.steampowered.com/app/' + d.appid;
const badges = dlcTypeBadgeHtml(d.type) + dlcBundleBadgeHtml(d.bundleCount) + dlcPlayedBadgeHtml(d.playtime);
return '' +
'
' + dlcCapsuleImg(d.appid, d.name) + '
' +
'
' +
'
' + Host.wlEscape(d.name) + ' ' + d.appid + '
' +
'
' + badges + '
' +
'
' +
'
';
});
const row = content.querySelector('.sglv-dlc-group[data-group-idx="' + gi + '"] .sglv-dlc-card-row');
if (row) appendDlcCardsChunked(row, cardHtmls);
});
}
// ==================== 捆绑包标签页 (v1.9.14) ====================
// 数据源: Host.getBundleCount(appid) — DLC 进包次数 (Barter.vg)
// Host.getDlcParent(appid) — DLC→父游戏 appID
// Host.getDlcsByParentMap() — 父游戏→DLC列表 反向映射
// 展示: 已拥有游戏中,包含进包 DLC 的父游戏列表,以及直接进包的游戏
// 构建捆绑包数据: 扫描所有已拥有父游戏的 DLC, 找出进包 DLC, 按父游戏分组
function buildBundleData() {
const t0 = performance.now();
const allOwned = Host.getOwnedGames() || [];
const showFamilyShared = Host.getShowFamilyShared();
const pool = showFamilyShared ? allOwned : allOwned.filter(g => Host.isGameOwnedByMe(g));
// 构建 appid → game 映射 (O(1) 查找游戏名称/拥有状态)
const gameMap = {};
pool.forEach(g => { if (g && g.appid) gameMap[String(g.appid)] = g; });
// 全量 owned 用于查父游戏名称 (即使父游戏不在当前 pool)
allOwned.forEach(g => { if (g && g.appid && !gameMap[String(g.appid)]) gameMap[String(g.appid)] = g; });
const hasBundleCount = typeof Host.getBundleCount === 'function';
const dlcsByParent = (typeof Host.getDlcsByParentMap === 'function') ? Host.getDlcsByParentMap() : {};
const parentBundleMap = {}; // parentAppid → bundle item
// 1. 遍历父游戏→DLC列表, 找出进包 DLC
for (const [parentApp, dlcIds] of Object.entries(dlcsByParent)) {
if (!Array.isArray(dlcIds)) continue;
let totalBundle = 0;
const bundledDlcs = [];
for (const dlcId of dlcIds) {
const bc = hasBundleCount ? Host.getBundleCount(dlcId) : 0;
if (bc > 0) {
totalBundle += bc;
const dlcGame = gameMap[String(dlcId)];
bundledDlcs.push({
appid: dlcId,
name: dlcGame ? dlcGame.name : ('DLC ' + dlcId),
bundleCount: bc,
isOwned: !!dlcGame,
playtime: dlcGame ? (dlcGame.playtime || 0) : 0,
});
}
}
if (totalBundle > 0) {
const parentGame = gameMap[String(parentApp)];
parentBundleMap[parentApp] = {
appid: Number(parentApp),
name: parentGame ? parentGame.name : ('App ' + parentApp),
isOwned: !!parentGame && pool.some(g => String(g.appid) === String(parentApp)),
totalBundle: totalBundle,
dlcCount: bundledDlcs.length,
dlcs: bundledDlcs,
};
}
}
// 2. 检查已拥有游戏本身是否也进包 (getBundleCount 对主游戏可能也有效)
for (const g of pool) {
if (!g || !g.appid) continue;
const bc = hasBundleCount ? Host.getBundleCount(g.appid) : 0;
if (bc <= 0) continue;
const key = String(g.appid);
if (!parentBundleMap[key]) {
// 主游戏直接进包, 且没有子 DLC 进包
parentBundleMap[key] = {
appid: g.appid,
name: g.name,
isOwned: true,
totalBundle: bc,
dlcCount: 0,
dlcs: [],
};
} else {
// 主游戏既自身进包, 子 DLC 也进包——把本体作为第一条 DLC 记录
parentBundleMap[key].totalBundle += bc;
parentBundleMap[key].dlcs.unshift({
appid: g.appid,
name: g.name + ' ' + (Host.isZh ? '(本体)' : '(Base)'),
bundleCount: bc,
isOwned: true,
playtime: g.playtime || 0,
});
parentBundleMap[key].dlcCount++;
}
}
// 按累计进包数降序作为默认排序
bundleState.data = Object.values(parentBundleMap).sort((a, b) => b.totalBundle - a.totalBundle);
bundleState.loaded = true;
console.log(`[SGLV-Bundle] buildBundleData — ${bundleState.data.length} 个含进包游戏, 总进包 DLC ${bundleState.data.reduce((s, g) => s + g.dlcCount, 0)} 个, 耗时 ${Math.round(performance.now() - t0)}ms`);
}
// 过滤+排序
function getFilteredBundle() {
let list = bundleState.data.slice();
// 搜索过滤 (游戏名 / appid / DLC名)
if (bundleState.search) {
const q = bundleState.search.toLowerCase();
list = list.filter(item =>
String(item.name).toLowerCase().includes(q) ||
String(item.appid).includes(q) ||
item.dlcs.some(d => String(d.name).toLowerCase().includes(q))
);
}
// 排序
const sort = bundleState.sort;
try {
list.sort((a, b) => {
if (sort === 'bundleDesc') return b.totalBundle - a.totalBundle || b.dlcCount - a.dlcCount;
if (sort === 'bundleAsc') return a.totalBundle - b.totalBundle || a.dlcCount - b.dlcCount;
if (sort === 'nameAsc') return _nameCollator.compare(a.name, b.name);
if (sort === 'appidDesc') return b.appid - a.appid;
if (sort === 'appidAsc') return a.appid - b.appid;
return 0;
});
} catch (e) {
// 降级: 按 appid 排序
list.sort((a, b) => b.appid - a.appid);
}
return list;
}
// 渲染 KPI 卡片
function renderBundleKpi() {
const el = document.getElementById('sglv-bundle-kpi');
if (!el) return;
if (!bundleState.loaded) { el.innerHTML = ''; return; }
const data = bundleState.data;
const totalGames = data.length;
const totalDlcs = data.reduce((s, g) => s + g.dlcCount, 0);
const totalBundles = data.reduce((s, g) => s + g.totalBundle, 0);
const ownedGames = data.filter(g => g.isOwned).length;
const ownPct = totalGames > 0 ? Math.round(ownedGames / totalGames * 100) : 0;
el.innerHTML = `
${totalGames.toLocaleString()}
${Host.isZh ? '已拥有 ' + ownedGames.toLocaleString() + ' (' + ownPct + '%)' : 'Owned ' + ownedGames.toLocaleString() + ' (' + ownPct + '%)'}
${totalDlcs.toLocaleString()}
${Host.isZh ? '含 Bundle 标记' : 'In Bundles'}
${totalBundles.toLocaleString()}
${Host.isZh ? '所有 DLC 进包次数之和' : 'Sum of all bundle counts'}
`;
}
// 渲染游戏列表 (卡片视图)
function renderBundleList() {
const content = document.getElementById('sglv-bundle-content');
if (!content) return;
const clearPagination = () => { const p = document.getElementById('sglv-bundle-pagination'); if (p) p.innerHTML = ''; };
// 检查 DLC 数据库是否可用
if (typeof Host.isDlcDbReady === 'function' && !Host.isDlcDbReady()) {
content.innerHTML = '' +
(Host.isZh ? 'DLC 数据库尚未加载,请先打开「DLC」标签页加载数据后返回。' : 'DLC database not loaded. Please open the "DLC" tab first to load data, then come back.') +
'
';
clearPagination();
return;
}
if (!bundleState.loaded || bundleState.data.length === 0) {
content.innerHTML = '' + (Host.isZh ? '暂无进包数据' : 'No bundle data available') + '
';
clearPagination();
return;
}
const list = getFilteredBundle();
if (list.length === 0) {
content.innerHTML = '' + (Host.isZh ? '无匹配结果' : 'No matching results') + '
';
clearPagination();
return;
}
const { pageItems, totalPages, page: _clampedPage } = Host.paginate(list, bundleState.page, bundleState.pageSize);
bundleState.page = _clampedPage;
const bundleBadgeStyle = 'background:rgba(245,158,11,0.15);color:#fbbf24;border:1px solid rgba(245,158,11,0.25);font-size:10px;padding:2px 6px;border-radius:3px;font-weight:700';
const dlcBadgeStyle = 'background:var(--sglv-accent-bg, rgba(167,139,250,0.15));color:var(--sglv-accent, #a78bfa);border:1px solid var(--sglv-accent-border, rgba(167,139,250,0.25));font-size:10px;padding:2px 6px;border-radius:3px;font-weight:700';
content.innerHTML = '' + pageItems.map(item => {
const clickUrl = 'https://store.steampowered.com/app/' + item.appid;
const bundleBadge = '
📦' + item.totalBundle + ' ';
const dlcBadge = item.dlcCount > 0
? '
' + item.dlcCount + ' DLC '
: '';
const posterHtml = Host.posterImg(item.appid, item.name);
// DLC 详情列表 (最多预览 5 条)
let dlcListHtml = '';
if (item.dlcs.length > 0) {
const preview = item.dlcs.slice(0, 5);
const moreCount = item.dlcs.length - preview.length;
dlcListHtml = '
' +
preview.map(d => {
const dlcUrl = 'https://store.steampowered.com/app/' + d.appid;
return '
' +
'' + Host.wlEscape(d.name) + ' ' +
'📦' + d.bundleCount + ' ' +
'
';
}).join('') +
(moreCount > 0 ? '
' + (Host.isZh ? '还有 ' + moreCount + ' 个…' : '+' + moreCount + ' more…') + '
' : '') +
'
';
}
return '
' +
'
' + posterHtml + '
' +
'
' +
'
' +
'' + Host.wlEscape(item.name) + ' ' +
'' + item.appid + ' ' +
'
' +
'
' + bundleBadge + dlcBadge + '
' +
dlcListHtml +
'
' +
'
';
}).join('') + '
';
// 分页
// v1.9.47-fix: 补 onJump——捆绑包翻页 5 个按钮全部走 onJump(参见主脚本 renderPagination)
const pagination = document.getElementById('sglv-bundle-pagination');
if (pagination) {
Host.renderPagination(pagination, 'sglv-bundle', bundleState.page, totalPages, list.length,
() => { bundleState.page--; renderBundleList(); },
() => { bundleState.page++; renderBundleList(); },
(p) => { bundleState.page = p; renderBundleList(); });
}
}
// 捆绑包标签页主渲染入口
function renderBundleTab(parent) {
const wrap = document.createElement('div');
wrap.className = 'sglv-psplus-wrap';
// KPI 区域
const kpi = document.createElement('div');
kpi.id = 'sglv-bundle-kpi';
wrap.appendChild(kpi);
// 工具栏: 搜索 + 排序
const toolbar = document.createElement('div');
toolbar.className = 'sglv-toolbar';
const sortOptions = [
{ key: 'bundleDesc', label: Host.isZh ? '进包数↓' : 'Bundles↓' },
{ key: 'bundleAsc', label: Host.isZh ? '进包数↑' : 'Bundles↑' },
{ key: 'nameAsc', label: Host.isZh ? '名称 A-Z' : 'Name A-Z' },
{ key: 'appidDesc', label: Host.isZh ? 'AppID↓' : 'AppID↓' },
{ key: 'appidAsc', label: Host.isZh ? 'AppID↑' : 'AppID↑' },
];
toolbar.innerHTML = `
${sortOptions.map(o => `${o.label} `).join('')}
`;
wrap.appendChild(toolbar);
// 游戏列表容器
const content = document.createElement('div');
content.className = 'sglv-content';
content.id = 'sglv-bundle-content';
wrap.appendChild(content);
parent.appendChild(wrap);
// 事件绑定
toolbar.querySelector('#sglv-bundle-search').addEventListener('input', _debounce((e) => {
bundleState.search = e.target.value;
bundleState.page = 1;
renderBundleList();
}, 200));
toolbar.querySelector('#sglv-bundle-sort').addEventListener('change', (e) => {
bundleState.sort = e.target.value;
bundleState.page = 1;
renderBundleList();
});
// 构建数据并渲染
buildBundleData();
renderBundleKpi();
renderBundleList();
}
// ==================== 收藏浮窗管理(v2.7.0) ====================
function ensureCollectionPanel() {
if (collectionPanelEl) return;
collectionOverlayEl = document.createElement('div');
collectionOverlayEl.className = 'sglv-overlay sgc-overlay';
collectionOverlayEl.addEventListener('click', closeCollection);
document.body.appendChild(collectionOverlayEl);
collectionPanelEl = document.createElement('div');
collectionPanelEl.className = 'sglv-panel sgc-panel';
// v2.9.22: 重排页头——保留最左侧 Steam 图标作为品牌标识,6 个 tab 移入 header 内部(仿主面板 v2.9.22 紧凑风格)
collectionPanelEl.innerHTML = `
`;
document.body.appendChild(collectionPanelEl);
collectionPanelEl.querySelector('.sglv-close-btn').addEventListener('click', closeCollection);
// v1.8.0: home 按钮 → 关闭收藏浮窗并返回游戏库浮窗
collectionPanelEl.querySelector('.sglv-home-btn').addEventListener('click', () => {
closeCollection();
document.dispatchEvent(new CustomEvent('sglv:open-library'));
});
collectionPanelEl.querySelectorAll('.sglv-tab').forEach(tab => {
tab.addEventListener('click', () => {
_collectionActiveTab = tab.dataset.tab;
collectionPanelEl.querySelectorAll('.sglv-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
renderCollectionBody();
});
});
// v1.8.0: 系列分类数据异步加载完成后,若当前正在展示系列分类标签页则自动刷新
// v1.9.40: 移除 seriesFetchedAt 更新(更新时间文本已移除)
// v1.9.37: 不再调用 renderCollectionBody(会闪骨架),改为直接刷新侧边栏+详情
document.addEventListener('sglv:series-data-loaded', () => {
if (_collectionActiveTab === 'publisher' && collectionPanelEl && collectionPanelEl.classList.contains('sglv-show')) {
refreshPublisherTabContent();
}
});
// v2.9.91: 供应商数据远程更新事件——运行时检查发现远程版本有更新时自动刷新
// v1.9.37: 同上,改为直接刷新侧边栏+详情
document.addEventListener('sglv:supplier-data-loaded', () => {
if (_collectionActiveTab === 'publisher' && collectionPanelEl && collectionPanelEl.classList.contains('sglv-show')) {
refreshPublisherTabContent();
}
});
// v2.9.90: 锁区数据远程更新事件——后台时间戳检查发现新版本或强制刷新后,
// 自动更新收藏浮窗的 blockedState 并重新渲染(解决主面板刷新后收藏浮窗仍显示旧数据的问题)
document.addEventListener('sglv:blocked-data-updated', (e) => {
const detail = e.detail || {};
if (Array.isArray(detail.apps) && detail.apps.length > 0) {
blockedState.apps = detail.apps;
blockedState.source = detail.source || 'static';
blockedState.fetchedAt = detail.fetchedAt || '';
blockedState.loaded = true;
// 同步写入永久缓存
_permCacheSet('blockedApps', { apps: blockedState.apps, source: blockedState.source, fetchedAt: blockedState.fetchedAt });
// v2.9.91: 同时刷新好评500分类数据(确保与主数据同步)
loadTopRatedApps(true).finally(() => {
if (_collectionActiveTab === 'blocked' && collectionPanelEl && collectionPanelEl.classList.contains('sglv-show')) {
renderBlockedSections();
}
});
// 若收藏浮窗打开且当前在锁区标签页,重新渲染
if (_collectionActiveTab === 'blocked' && collectionPanelEl && collectionPanelEl.classList.contains('sglv-show')) {
renderBlockedSections();
}
}
});
// v2.9.22: 注入 sgc-panel 紧凑 CSS(仿主面板 v2.9.22:紧凑 header、tab 按钮圆角化、品牌图标、拖动 cursor、加载骨架)
if (typeof GM_addStyle === 'function' && !collectionPanelEl._sgcStyled) {
GM_addStyle(SGC_PANEL_CSS);
collectionPanelEl._sgcStyled = true;
}
// v2.9.22: 绑定拖动(仅一次)
if (!collectionPanelEl._sgcDraggable) {
makeSgcDraggable(collectionPanelEl, collectionPanelEl.querySelector('.sglv-header'));
collectionPanelEl._sgcDraggable = true;
}
// v2.7.0: 监听主脚本库存更新事件,收藏浮窗打开时自动刷新
document.addEventListener('sglv:games-updated', () => {
if (collectionPanelEl && collectionPanelEl.classList.contains('sglv-show')) {
renderCollectionBody();
}
});
}
function renderCollectionBody() {
const body = collectionPanelEl?.querySelector('#sgc-body');
if (!body) return;
// v1.9.37: 移除切 tab 骨架屏,直接渲染内容(避免界面变形/闪烁)
body.innerHTML = '';
if (_collectionActiveTab === 'delisted') renderDelistedTab(body);
else if (_collectionActiveTab === 'blocked') renderBlockedTab(body);
else if (_collectionActiveTab === 'goty') renderGotyTab(body);
else if (_collectionActiveTab === 'publisher') renderPublisherTab(body);
else if (_collectionActiveTab === 'psplus') renderPsplusTab(body);
else if (_collectionActiveTab === 'epic') renderEpicTab(body);
else if (_collectionActiveTab === 'dlc') renderDlcTab(body);
else if (_collectionActiveTab === 'bundle') renderBundleTab(body);
_collectionBodyInitialized = true;
}
function openCollection() {
ensureCollectionPanel();
collectionOverlayEl.classList.add('sglv-show');
collectionPanelEl.classList.add('sglv-show');
renderCollectionBody();
}
function closeCollection() {
if (collectionOverlayEl) collectionOverlayEl.classList.remove('sglv-show');
if (collectionPanelEl) collectionPanelEl.classList.remove('sglv-show');
}
function toggleCollection() {
if (collectionPanelEl && collectionPanelEl.classList.contains('sglv-show')) closeCollection();
else openCollection();
}
return {
init,
apiVersion,
openCollection,
closeCollection,
toggleCollection,
renderPublisherTab,
renderDelistedTab,
renderPsplusTab,
renderEpicTab,
renderBlockedTab,
renderGotyTab,
renderDlcTab,
renderBundleTab
};
})();
// ============================================================================
// end of inlined sglv-game-collection 1.9.47
// ============================================================================
(function () {
'use strict';
// v2.9.54: 顶层环境守卫 — keylol 等论坛帖子内的 Steam 商店链接卡片以 iframe 形式嵌入
// store.steampowered.com 页面,@match 会命中 iframe 内脚本上下文,导致侧边栏侧滑按钮(sgis-fab)
// 检测 window.self !== window.top 时直接退出,跳过所有 UI 初始化。
if (window.self !== window.top) return;
const _v = typeof GM_info !== 'undefined' ? GM_info.script.version : 'unknown';
// v2.11.40: 主题层——启动日志移至 init()(需读 storage.getUiTheme,此处尚在 TDZ 之前)
// ==================== v2.8.3: Toast 非阻塞通知系统 ====================
const sglvToast = {
_el: null,
_timer: null,
_show(msg, type) {
if (!this._el) {
this._el = document.createElement('div');
this._el.id = 'sglv-toast';
document.body.appendChild(this._el);
}
this._el.textContent = msg;
this._el.className = 'sglv-toast' + (type ? ' sglv-toast-' + type : '');
requestAnimationFrame(() => this._el.classList.add('sglv-toast-show'));
if (this._timer) clearTimeout(this._timer);
this._timer = setTimeout(() => this._el.classList.remove('sglv-toast-show'), 2500);
},
success(msg) { this._show(msg, 'success'); },
error(msg) { this._show(msg, 'error'); },
warning(msg) { this._show(msg, 'warning'); },
info(msg) { this._show(msg, 'info'); }
};
// ==================== v2.9.29: 全局 HTML 转义函数(防 XSS) ====================
// 统一 HTML 转义工具,所有 innerHTML 拼接外部数据时必须调用
function escHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// v2.9.85: 货币代码→符号映射(参考 steam-family-game-analysis faCurrencySymbol)
// SGLVAppDetail.loadDetail 用 cc=cn 请求 appdetails,但 Steam 可能对登录用户忽略 cc= 参数,
// 返回用户实际所在地区的价格(如 INR),需根据 d.cnPrice.currency 字段选择正确符号,
// 而非旧代码硬编码的 ¥。置于主作用域,供搜索详情浮窗(_renderDetailContent)直接调用。
// v2.9.100: 起优先委托 sglv-currency.lib.js 的 SGLVCurrency.symbolOf,lib 缺失时回退下方内置 map(行为不变)。
// 注意: 本函数定义早于下方 CUR 探测声明,故在函数内部直接探测全局,不依赖外层变量。
function sglvCurrencySymbol(code) {
try {
const lib = (typeof unsafeWindow !== 'undefined' && unsafeWindow.SGLVCurrency) || window.SGLVCurrency;
if (lib && lib.apiVersion === 1) return lib.symbolOf(code);
} catch (e) { console.debug('[SGLV] SGLVCurrency.symbolOf 异常,降级内置 map', e); }
const map = {
'CNY': '¥', 'USD': '$', 'EUR': '€', 'GBP': '£', 'JPY': '¥', 'KRW': '₩',
'INR': '₹', 'RUB': '₽', 'BRL': 'R$', 'TRY': '₺', 'AUD': 'A$', 'CAD': 'C$',
'HKD': 'HK$', 'TWD': 'NT$', 'SGD': 'S$', 'THB': '฿', 'MYR': 'RM', 'PHP': '₱',
'IDR': 'Rp', 'VND': '₫', 'MXN': 'Mex$', 'ARS': 'ARS$', 'CLP': 'CLP$', 'COP': 'COL$',
'PEN': 'S/', 'ZAR': 'R', 'UAH': '₴', 'PLN': 'zł', 'NOK': 'kr', 'CHF': 'CHF ',
'NZD': 'NZ$', 'SAR': 'SR', 'AED': 'AED ', 'ILS': '₪', 'KZT': '₸', 'QAR': 'QR',
'KWD': 'KD', 'CRC': '₡', 'UYU': '$U'
};
return map[String(code || '').toUpperCase()] || '¥';
}
// ==================== v2.11.0 精简: SGLVCore 依赖注入 ====================
// v2.11.0: 移除 2.10.0 SGLVCore 降级实现(~194 行)— @require 外部库加载失败即终止核心功能,
// 不再尝试用内置最小化实现(之前 IDB→内存+GM, negCache→内存TTL 等)以减少同步成本。
// 业务代码继续按原 const/function 名字使用,零改动。IDB / 负缓存 / StageProgress /
// concurrentPool / gmFetch / notifyNetFail 统一来自 sglv-core.lib.js v1.0.6。
let C = (typeof unsafeWindow !== 'undefined' && unsafeWindow.SGLVCore) || window.SGLVCore;
// v2.9.114: 终态失败通知包装——保留兜底以兼容 core 旧版本 < 1.0.5 (远端发布滞后场景),
// 缺失时回退到 console.warn 静默输出,保证 catch 块内调用永不抛错。
const sglvNotifyNetFail = (typeof C.notifyNetFail === 'function')
? C.notifyNetFail.bind(C)
: (label, err) => { try { console.warn('[SGLV] 数据获取失败(系统通知未接入):', label, (err && err.message) || err); } catch (e2) {} };
// v2.9.100: SGLV Currency 货币库软降级探测(增强能力,非基础设施:缺失仅降级不早退)
const CUR = (typeof unsafeWindow !== 'undefined' && unsafeWindow.SGLVCurrency) || window.SGLVCurrency;
const hasSglvCurrency = !!(CUR && CUR.apiVersion === 1);
if (!hasSglvCurrency) console.warn('[SGLV] SGLVCurrency 未加载或版本不匹配,货币增强功能降级为内置实现');
// ----- 命名空间 -----
const nsKey = C.nsKey;
// ----- IDB KV 库 -----
const sglvIDB = C.IDB;
// ----- 缓存 schema 版本号(业务特定默认值,本脚本独有) -----
const CACHE_VERSION = {
bundle_db: 2, // 旧版大体积缓存需要 v2 精简
dlc_db: 1,
app_types: 1,
cover_good_url: 1,
game_name: 1,
blocked_apps: 1,
};
function ensureCacheVersions() { return C.ensureCacheVersions(CACHE_VERSION); }
// ----- 负缓存(直接转发 core 暴露的 API) -----
const negCacheGet = C.negCache.get.bind(C.negCache);
const negCacheSet = C.negCache.set.bind(C.negCache);
const negCacheClear = C.negCache.clear.bind(C.negCache);
// ----- 阶段进度 / 节流 / 并发 / 抓取 / 占位 -----
const StageProgress = C.StageProgress;
const concurrentPool = C.concurrentPool;
// ----- GM 网络包装(原 sglvGmFetchRetry / sglvGmFetchTextRetry) -----
const sglvGmFetchRetry = C.gmFetchJson;
const sglvGmFetchTextRetry = C.gmFetchText;
// ==================== v2.8.3: 通用 UI 组件函数 ====================
// v2.11.3 (FEAT-011 TASK-004): spinner 段生成器——默认形态(#a78bfa/自带自旋动画)与
// 专属 class 形态(currentColor/圆角端点,尺寸与动画交给配套 CSS,如 .sglv-wl-spin);
// 供 createLoadingHtml 工厂与详情弹窗加载态共用,消除手写 svg 重复
function spinSvg(size = 34, cls = '') {
if (cls) {
return ` `;
}
return ` `;
}
// v2.11.3 (FEAT-011 TASK-004): 阶段条段生成器——自 SGIS renderProgressLoading 收编
// (class 序列逐字节等价,含非激活态 class 属性的尾随空格;wrapId 供增量更新器按 id 定位)
function stageBarsHtml(total, active, wrapId = '') {
const bars = Array.from({ length: total }, (_, i) =>
`
`
).join('');
return `${bars}
`;
}
// v2.11.3 (FEAT-011 TASK-004): 加载态工厂扩展——单一加载态入口,opts 全部可选
// (无 opts 时输出与 2.11.2 逐字节一致):
// cls 外层附加 class(如 'sglv-wl-loading';给定时外层不再输出内联布局样式,布局交给 CSS)
// svgCls spinner 段专属 class(如 'sglv-wl-spin',currentColor 形态,尺寸由 CSS 决定)
// spin 'none' 时不渲染 spinner 段
// textCls 文案附加 class(给定时文案用 ,否则维持内联
)
// stages [{key,label}] + activeStage(+可选 stagesId) → 阶段条(数组长度即阶段数,label 预留)
// progress {pct} 或 {current,total} → 进度条(total=0 除零保护为 0%;宽度公式与愿望单现状一致)
// sub 进度条下副文案
// 进度条/副文案复用愿望单 .sglv-wl-loading-bar/-fill/-sub 既有 class 体系(CSS 不动)
function createLoadingHtml(text, size = 34, opts = null) {
const svg = (!opts || opts.spin !== 'none') ? spinSvg(size, opts ? (opts.svgCls || '') : '') : '';
const textEl = opts && opts.textCls
? `${text}
`
: `${text} `;
let inner = svg + textEl;
if (opts && opts.stages) {
const active = opts.activeStage != null ? opts.activeStage : opts.stages.length;
inner += stageBarsHtml(opts.stages.length, active, opts.stagesId || '');
}
if (opts && opts.progress) {
const p = opts.progress;
const raw = p.pct != null ? p.pct
: (p.total > 0 ? Math.round(p.current / p.total * 100) : 0);
const pct = Math.max(0, Math.min(100, raw));
inner += ``;
}
if (opts && opts.sub != null) inner += `${opts.sub}
`;
if (opts && opts.cls) return `${inner}
`;
return `${inner}
`;
}
// ==================== 供应商-系列-游戏数据库 ====================
// 从远程 @resource 加载,数据源:Azure Blob Storage
// v2.9.87: 数据升级 game-db-v1.2.json——光荣特库摩重编为 19 个系列分类(数据源小黑盒帖子),
// 游戏条目新增可选 genres 字段(Steam appdetails 类型标签),由收藏模块 v1.9.16 渲染展示
const GAME_DB = (function() {
try {
const raw = JSON.parse(GM_getResourceText('gameDb') || '{}');
// 移除 _meta 元数据,避免被解析为供应商分类
const { _meta, ...db } = raw;
return db;
}
catch(e) { console.warn('[SGLV] GAME_DB 加载失败:', e); return {}; }
})();
const DELISTED_DB = (function() {
try { return JSON.parse(GM_getResourceText('delistedData') || '{}'); }
catch(e) { console.warn('[SGLV] DELISTED_DB 加载失败:', e); return {}; }
})();
// ==================== v2.9.27: 游戏系列数据(game_series.json) ====================
// 数据源: https://raw.githubusercontent.com/SmallRob/steam-namespace/main/data/game_series.json
// 经 jsdelivr CDN 加速: https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/game_series.json
// (数据由 py_scripts/import_heybox_series.py 等脚本维护,与锁区数据同仓库)
// 兼容两种格式: 对象 { 系列名: [{序号, date, appid, name}] }(当前线上格式)
// / 扁平数组 [{name, date, appid, 序号, 系列}](旧格式,按"系列"字段分组)
const SERIES_DATA_URL = 'https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/game_series.json';
// v2.9.104: 互动影游数据源(steam_vediogame_selected.json,扁平数组 [{序号, date, appid, name}])
// 数据源: https://raw.githubusercontent.com/SmallRob/steam-namespace/main/data/steam_vediogame_selected.json
// 经 jsdelivr CDN 加速(与系列数据同仓库同管线)
const VEDIO_SERIES_DATA_URL = 'https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/steam_vediogame_selected.json';
// v2.9.113: GitHub 原始源(CDN 失败时回退;force 刷新绕过 jsdelivr 边缘缓存,确保拿到最新分类数据)
const SERIES_DATA_URL_RAW = 'https://raw.githubusercontent.com/SmallRob/steam-namespace/main/data/game_series.json';
const VEDIO_SERIES_DATA_URL_RAW = 'https://raw.githubusercontent.com/SmallRob/steam-namespace/main/data/steam_vediogame_selected.json';
// v2.9.27: 独立系列库 { seriesName: [{id, name, date}] },与 GAME_DB(供应商)分开展示
let seriesDb = {};
// v2.9.104: 最近一次成功拉取/缓存的合并原始数据——某一数据源失败时,
// 从内存兜底恢复,避免系列侧边栏因单源失败而塌缩/丢系列
let _lastSeriesRaw = null;
// v2.9.91: 供应商数据远程覆盖(运行时检查 @resource gameDb 远程版本,有更新时覆盖 GAME_DB)
const GAME_DB_REMOTE_URL = 'https://fastly.jsdelivr.net/gh/SmallRob/steam-namespace@main/data/game-db-v1.2.json';
// v2.9.113: 供应商数据 GitHub 原始源(CDN 失败回退)
const GAME_DB_REMOTE_URL_RAW = 'https://raw.githubusercontent.com/SmallRob/steam-namespace/main/data/game-db-v1.2.json';
let supplierDbOverride = null;
let supplierFetchedAt = '';
/**
* 将 game_series.json 规范化为独立系列库(不做二级分类,不与供应商合并)
* 兼容对象格式 { 系列名: [{序号, date, appid, name}] } 与扁平数组 [{name, date, appid, 序号, 系列}]
* 返回: { seriesName: [{id, name, date, order}, ...] }(系列内按序号升序)
*/
function groupSeriesData(seriesData) {
const grouped = {};
const pushGame = (seriesName, item) => {
const id = Number(item.appid) || 0;
if (!id) return;
if (!grouped[seriesName]) grouped[seriesName] = [];
// 同系列内按 appid 去重
if (grouped[seriesName].some(g => g.id === id)) return;
grouped[seriesName].push({
id,
name: item.name || '',
date: item.date || '',
order: Number(item['序号'] || item.order) || 0,
// v2.9.105: 保留数据源 poster 字段(预验证封面URL,为空时走 CDN 降级链)
poster: item.poster || '',
});
};
if (Array.isArray(seriesData)) {
// 扁平数组格式:按"系列"字段分组
for (const item of seriesData) {
const seriesName = String(item['系列'] || item.series || '').trim() || '未分类';
pushGame(seriesName, item);
}
} else if (seriesData && typeof seriesData === 'object') {
// 对象格式:键即系列名
for (const [seriesName, games] of Object.entries(seriesData)) {
const name = String(seriesName).trim();
if (!name || !Array.isArray(games)) continue;
for (const item of games) pushGame(name, item);
}
}
// 系列内按序号升序(无序号的排后,保持稳定排序)
for (const games of Object.values(grouped)) {
games.sort((a, b) => (a.order || Infinity) - (b.order || Infinity));
}
return grouped;
}
/**
* v2.9.104: 合并系列数据源——互动影游数据(扁平数组)作为独立"互动影游"系列并入 game_series 数据
* 归一为 groupSeriesData 可直接消费的格式(对象 { 系列名: [...] } / 旧扁平数组打"系列"标记),
* 使互动影游走与既有系列完全相同的缓存/签名/事件管线(7 天 TTL、签名对比、强制刷新、状态三态)。
* @param {any} mainData - game_series.json 原始数据(对象或旧扁平数组)
* @param {any} vedioData - steam_vediogame_selected.json 原始数据(扁平数组)
* @returns {any} 合并后的原始数据(未归一化)
*/
function mergeVedioSeriesData(mainData, vedioData) {
if (!Array.isArray(vedioData) || !vedioData.length) return mainData;
if (Array.isArray(mainData)) {
// 旧扁平数组格式:给条目打上"系列"标记后并入(与旧数据兼容)
return mainData.concat(vedioData.map(item => Object.assign({}, item, { 系列: '互动影游' })));
}
if (mainData && typeof mainData === 'object') {
const out = Object.assign({}, mainData);
// 影游专用文件为互动影游权威数据源,覆盖主文件同名键(若有)
out['互动影游'] = vedioData;
return out;
}
// 主数据源为空/异常但影游数据有效:包装成对象格式兜底
return { '互动影游': vedioData };
}
/**
* v2.9.113: djb2 轻量哈希——将 appid 列表折叠为紧凑签名,避免超长字符串
*/
function _sigHash(str) {
let h = 5381;
for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) | 0;
return (h >>> 0).toString(36);
}
/**
* v2.9.90: 计算系列数据内容签名(用于检测远程数据是否有变化)
* v2.9.113: 签名从"系列数:游戏总数"粗粒度升级为逐系列 appid 集合哈希——
* 此前互动影游等分类的条目级变更(游戏新增/移除/跨系列移动而总数恰好不变)无法检测,
* 导致手动刷新后分类数据"看似成功实则未更新"
*/
function _computeSeriesSignature(data) {
if (Array.isArray(data)) {
const ids = data.map(i => String(i && (i.appid != null ? i.appid : i.name) || '')).sort();
return `arr:${data.length}:${_sigHash(ids.join(','))}`;
} else if (data && typeof data === 'object') {
const keys = Object.keys(data).sort();
const parts = keys.map(k => {
const arr = Array.isArray(data[k]) ? data[k] : [];
const ids = arr.map(i => String(i && (i.appid != null ? i.appid : i.name) || '')).sort();
return `${k}(${arr.length}):${_sigHash(ids.join(','))}`;
});
let totalGames = 0;
for (const k of keys) if (Array.isArray(data[k])) totalGames += data[k].length;
return `obj:${keys.length}:${totalGames}:${_sigHash(parts.join('|'))}`;
}
return 'empty';
}
/**
* v2.9.113: GM GET JSON(无重试)——raw.githubusercontent 原始源回退用
* v2.11.36 (FEAT-015 TASK-003): 迁移至 sglvGmFetchRetry(retries:0 保持无重试语义,Accept 头透传)
*/
function _gmGetJsonRaw(url, timeout = 15000) {
return sglvGmFetchRetry(url, { timeout, retries: 0, headers: { 'Accept': 'application/json' } });
}
/**
* v2.9.113: 收藏分类数据源抓取——CDN 主源 + GitHub 原始源回退
* force 时附加时间戳查询参数绕过 jsdelivr 边缘缓存(不同查询串为不同缓存键,
* 强制回源拉最新数据,解决手动刷新拿到 CDN 旧快照导致互动影游等分类不更新的问题);
* 非 force 走普通 URL 复用 CDN 缓存(快、省流量)。
* @returns {Promise} 解析后的 JSON
*/
async function _fetchCollectionJson(cdnUrl, rawUrl, force) {
const bust = force ? `?_sglv=${Date.now()}` : '';
try {
return await sglvGmFetchRetry(cdnUrl + bust, { timeout: 15000 });
} catch (e) {
return await _gmGetJsonRaw(rawUrl + bust, 15000);
}
}
/**
* 异步加载游戏系列数据(带缓存,先读缓存同步分组,再后台拉取最新)
* v2.9.90: 添加 force 参数 + 内容签名比较,避免无变化时重复写缓存/派发事件
* v2.9.92: force 时先移出过期缓存,返回结果状态 { status: 'updated'|'nochange'|'error', msg }
* v2.9.113: 缓存策略重写——
* ① force 不再先删缓存:拉取失败时旧缓存原样保留(此前先删后拉,失败即丢缓存,
* 内存兜底 _lastSeriesRaw 页面刷新后即失效)
* ② 拉取成功且签名变化时 cacheSet 覆盖写缓存(旧版本缓存被新版本替换)
* ③ force 附加时间戳绕 CDN 边缘缓存 + raw.githubusercontent 回退,确保拿到最新分类数据
* @param {boolean} force - 强制刷新(跳过缓存读取,始终拉取远程数据,绕 CDN 缓存)
* @param {function} onProgress - v2.9.91 (text) 进度回调
* @returns {Promise<{status:string, msg:string}>}
*/
async function loadSeriesData(force = false, onProgress) {
const report = (text) => { if (typeof onProgress === 'function') onProgress(text); };
// 1. 非强制刷新时先读缓存,同步分组(确保 UI 首次渲染即有系列数据)
if (!force) {
const cached = cacheGet('seriesData');
if (cached) {
const cachedDb = groupSeriesData(cached);
if (Object.keys(cachedDb).length > 0) {
seriesDb = cachedDb;
_lastSeriesRaw = cached;
document.dispatchEvent(new CustomEvent('sglv:series-data-loaded'));
}
}
}
// 2. 拉取最新远程数据(v2.9.104: game_series.json 与 steam_vediogame_selected.json 并行,
// 互动影游并入同一 seriesDb,共享缓存/签名/事件管线,更新逻辑与既有系列完全一致;
// 网络层统一走 SGLVCore gmFetchJson(自动重试 + 状态码校验 + JSON 解析),
// v2.9.113: 失败回退 raw.githubusercontent 原始源)
try {
report(isZh ? '正在加载系列数据…' : 'Fetching series data…');
const [mainRes, vedioRes] = await Promise.allSettled([
_fetchCollectionJson(SERIES_DATA_URL, SERIES_DATA_URL_RAW, force),
_fetchCollectionJson(VEDIO_SERIES_DATA_URL, VEDIO_SERIES_DATA_URL_RAW, force),
]);
let mainData = mainRes.status === 'fulfilled' ? mainRes.value : null;
let vedioData = vedioRes.status === 'fulfilled' ? vedioRes.value : null;
if (!mainData && !vedioData) {
const err = (mainRes.status === 'rejected' ? mainRes.reason : vedioRes.reason);
throw (err instanceof Error ? err : new Error('All series sources failed'));
}
if (mainRes.status === 'rejected') {
// 主源失败:优先复用缓存主数据,其次内存兜底
console.warn('[SGLV] 系列分类主数据源加载失败:', mainRes.reason && mainRes.reason.message);
const cachedMain = cacheGet('seriesData') || _lastSeriesRaw;
if (cachedMain) mainData = cachedMain;
else if (vedioData) mainData = {};
}
if (vedioRes.status === 'rejected') {
console.warn('[SGLV] 互动影游数据加载失败:', vedioRes.reason && vedioRes.reason.message);
// 影游源失败:复用缓存/内存兜底中的互动影游数据(保留上次成功拉取内容,避免刷新导致该系列消失)
const cachedRaw = cacheGet('seriesData') || _lastSeriesRaw;
if (cachedRaw && !Array.isArray(cachedRaw) && Array.isArray(cachedRaw['互动影游'])) {
vedioData = cachedRaw['互动影游'];
}
}
const data = mergeVedioSeriesData(mainData, vedioData);
const freshDb = groupSeriesData(data);
if (Object.keys(freshDb).length > 0) {
// v2.9.104: 内存兜底随每次成功拉取更新(后续单源失败时可用)
_lastSeriesRaw = data;
// v2.9.90: 内容签名比较——签名相同时仅刷新TTL,不派发事件(避免不必要的UI重渲染)
// v2.9.113: 签名为逐系列 appid 哈希,条目级变更也能检测;force 与否均按签名判定
const newSig = _computeSeriesSignature(data);
const cachedRaw = cacheGet('seriesData');
const oldSig = cachedRaw ? _computeSeriesSignature(cachedRaw) : '';
if (newSig !== oldSig) {
// 成功获取最新版本:新数据覆盖写缓存(旧版本缓存被替换清除)
cacheSet('seriesData', data, CACHE_TTL.seriesData);
seriesDb = freshDb;
document.dispatchEvent(new CustomEvent('sglv:series-data-loaded'));
let totalGames = 0;
for (const games of Object.values(freshDb)) totalGames += games.length;
console.log(`[SGLV] 系列分类数据${force ? '强制' : ''}加载成功: ${totalGames} 条,共 ${Object.keys(freshDb).length} 个系列 (签名: ${newSig})`);
return { status: 'updated', msg: isZh ? `系列: ${Object.keys(freshDb).length} 项已更新` : `Series: ${Object.keys(freshDb).length} items updated` };
} else {
// 签名相同,仅刷新缓存TTL,不派发事件
cacheSet('seriesData', data, CACHE_TTL.seriesData);
console.log('[SGLV] 系列分类数据远程无变化,跳过更新');
return { status: 'nochange', msg: isZh ? '系列: 已是最新' : 'Series: up to date' };
}
}
return { status: 'nochange', msg: isZh ? '系列: 数据为空' : 'Series: empty' };
} catch (e) {
// 拉取失败:缓存不动(旧缓存保留,UI 继续用缓存/内存数据)
console.warn('[SGLV] 系列分类数据加载失败:', e.message);
sglvNotifyNetFail(isZh ? '收藏分类数据' : 'Collection data', e); // v2.9.114: 终态失败系统通知(60s 节流)
return { status: 'error', msg: isZh ? `系列: ${e.message}` : `Series: ${e.message}` };
}
}
/**
* v2.9.91: 异步加载供应商数据(远程版本检查,有更新时覆盖 @resource GAME_DB)
* v2.9.92: force 时先移出过期缓存,返回结果状态 { status: 'updated'|'nochange'|'error', msg }
* v2.9.113: 缓存策略与系列数据同步重写——force 不再先删缓存(失败保留旧缓存)、
* 附加时间戳绕 CDN 边缘缓存 + raw.githubusercontent 回退
* @param {boolean} force - 强制刷新(跳过缓存读取,始终拉取远程数据,绕 CDN 缓存)
* @param {function} onProgress - (text) 进度回调
* @returns {Promise<{status:string, msg:string}>}
*/
async function loadSupplierData(force = false, onProgress) {
const report = (text) => { if (typeof onProgress === 'function') onProgress(text); };
// 非强制时检查缓存
if (!force) {
const cached = cacheGet('supplierData');
if (cached && cached.db) {
// 缓存命中:检查是否已有 override 或 GAME_DB 签名一致
const cachedSig = _computeSupplierSignature(cached.raw || {});
const currentSig = _computeSupplierSignature(GAME_DB);
if (cachedSig !== currentSig) {
supplierDbOverride = cached.db;
supplierFetchedAt = cached.fetchedAt || '';
document.dispatchEvent(new CustomEvent('sglv:supplier-data-loaded'));
}
}
}
// 拉取最新远程数据
try {
report(isZh ? '正在加载供应商数据…' : 'Fetching supplier data…');
const data = await _fetchCollectionJson(GAME_DB_REMOTE_URL, GAME_DB_REMOTE_URL_RAW, force);
// 移除 _meta 元数据
const { _meta, ...db } = data;
const newSig = _computeSupplierSignature(data);
const oldSig = _computeSupplierSignature(GAME_DB);
if (force || newSig !== oldSig) {
supplierDbOverride = db;
supplierFetchedAt = new Date().toISOString().slice(0, 10);
cacheSet('supplierData', { db, raw: data, fetchedAt: supplierFetchedAt }, CACHE_TTL.seriesData);
document.dispatchEvent(new CustomEvent('sglv:supplier-data-loaded'));
const totalGames = Object.values(db).reduce((s, v) => { try { return s + Object.values(v).reduce((ss, arr) => ss + (Array.isArray(arr) ? arr.length : 0), 0); } catch { return s; } }, 0);
console.log(`[SGLV] 供应商数据${force ? '强制' : ''}加载成功: ${Object.keys(db).length} 个供应商, ${totalGames} 个游戏 (签名: ${newSig})`);
return { status: 'updated', msg: isZh ? `供应商: ${Object.keys(db).length} 项已更新` : `Supplier: ${Object.keys(db).length} items updated` };
} else {
console.log('[SGLV] 供应商数据远程无变化,跳过更新');
return { status: 'nochange', msg: isZh ? '供应商: 已是最新' : 'Supplier: up to date' };
}
} catch (e) {
// 拉取失败:缓存与 override 不动
console.warn('[SGLV] 供应商数据加载失败:', e.message);
sglvNotifyNetFail(isZh ? '供应商分类数据' : 'Supplier data', e); // v2.9.114: 终态失败系统通知(60s 节流)
return { status: 'error', msg: isZh ? `供应商: ${e.message}` : `Supplier: ${e.message}` };
}
}
/** v2.9.91: 计算供应商数据内容签名(供应商数:游戏总数) */
function _computeSupplierSignature(data) {
if (!data || typeof data !== 'object') return 'empty';
const keys = Object.keys(data).filter(k => k !== '_meta');
let totalGames = 0;
for (const k of keys) {
if (typeof data[k] === 'object' && data[k] !== null) {
for (const sk of Object.keys(data[k])) {
if (Array.isArray(data[k][sk])) totalGames += data[k][sk].length;
}
}
}
return `pub:${keys.length}:${totalGames}`;
}
// ==================== SVG 图标 ====================
const _S = 'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
const ICONS = {
game: ` `,
grid: ` `,
list: ` `,
close: ` `,
settings: ` `,
refresh: ` `,
package: ` `,
trend: ` `,
barChart: ` `,
shield: ` `,
clock: ` `,
share: ` `,
// v2.9.67: 家庭共享图标(双人群组,用于愿望单家庭共享标签)
familyShare: ` `,
heart: ` `,
dollar: ` `,
tag: ` `,
gift: ` `,
library: ` `,
calendar: ` `,
// v2.7.2: 即将发售 KPI 图标(火箭,区别于日历图标的年份卡片)
rocket: ` `,
// v2.4.3: 横板封面视图图标(宽横幅卡片)
cover: ` `,
// v2.6.0: PlayStation 四符号图标(三角/圆/叉/方)用于 PS 会免标签页
// v2.9.22: 真实 PlayStation 标识(官方蓝紫色 P + S 经典组合),替代原 4 符号简笔
playstation: ` `,
// v2.7.9: Epic 赠送标签页图标("E" 字形)
// v2.9.22: Epic Games 盾形标识(深色盾形 + 白色 EPIC/GAMES 文字),替代原"E"字简笔
epic: `EPIC GAMES `,
// v2.9.0: 集换式卡牌图标
card: ` `,
// v2.9.7: 年度大作标签页图标(奖杯)
trophy: ` `,
// v2.9.93: 资料受限图标(用户轮廓 + 警告三角)
profileLimited: ` `,
// v2.9.33: 我的成就标签页图标(奖杯 + 星芒)
achievement: ` `,
// v2.9.34: 云存档标签页图标(云+下载箭头)
cloud: ` `,
// v2.9.32: DLC 收藏标签页图标(拼图块)
puzzle: ` `,
// v2.9.51: 全局搜索图标(放大镜)
search: ` `,
// v2.11.40: 主题层——外观设置卡调色板图标
palette: ` `,
// v2.9.51: 搜索结果跳转图标(外链箭头)
external: ` `,
// v2.9.90: 个人展柜标签页图标(展柜/陈列窗)
showcase: ` `,
// v2.9.90: 展柜编辑图标(铅笔)
edit: ` `,
// v2.9.91: 徽章图标(六角勋章 + 飘带)
medal: ` `,
};
// ==================== CSS 样式 ====================
// v2.7.1: CSS 经 @require 外部库 sglv-shared-css 加载,库未加载时为空字符串(UI 无样式但功能可用)
const CSS_STYLES = (typeof window.SGLVSharedCSS === 'string' && window.SGLVSharedCSS) || '';
GM_addStyle(CSS_STYLES);
// ==================== v2.11.0: 分散 CSS 集中提取 ====================
// 原 2.10.0 散落在 body 各处的 GM_addStyle(`...`) 块全部提取为命名常量,
// 一次性合并注入(CSS_INLINE)。与 4.7 精简模式对齐。
const CSS_ACHIEVEMENT =` /* ====== 主布局:左右分栏 ====== */
.sglv-ach-layout { display: flex; height: 100%; min-height: 400px; background: linear-gradient(180deg, #0d1117 0%, #161b22 100%); border-radius: 8px; overflow: hidden; }
/* ====== 左侧边栏 ====== */
.sglv-ach-sidebar { width: 240px; flex-shrink: 0; display: flex; flex-direction: column; background: linear-gradient(180deg, rgba(22,27,34,0.95) 0%, rgba(13,17,23,0.95) 100%); border-right: 1px solid rgba(48,54,61,0.6); overflow-y: auto; }
.sglv-ach-sidebar-header { display: flex; align-items: center; gap: 8px; padding: 16px 16px 12px; border-bottom: 1px solid rgba(48,54,61,0.4); }
.sglv-ach-sidebar-title { font-size: 15px; font-weight: 700; color: #e6edf3; display: flex; align-items: center; gap: 6px; }
.sglv-ach-sidebar-title svg { width: 20px; height: 20px; }
/* v2.9.59: 用户信息区 — 参考设计渐变背景+径向光晕 */
.sglv-ach-user-card { padding: 20px 16px; display: flex; flex-direction: column; align-items: center; gap: 6px; border-bottom: 1px solid rgba(48,54,61,0.4); background: linear-gradient(160deg, rgba(99,102,241,0.08), rgba(168,85,247,0.04)); position: relative; overflow: hidden; }
.sglv-ach-user-card::after { content: ''; position: absolute; inset: -50%; background: radial-gradient(circle, rgba(168,85,247,0.12), transparent 60%); pointer-events: none; }
.sglv-ach-user-avatar { width: 64px; height: 64px; border-radius: 50%; background: linear-gradient(135deg, #fde047, #d97706); display: flex; align-items: center; justify-content: center; flex-shrink: 0; position: relative; box-shadow: 0 8px 24px rgba(251,191,36,0.35); z-index: 1; }
.sglv-ach-user-avatar svg { width: 32px; height: 32px; }
.sglv-ach-user-level { position: absolute; bottom: -4px; right: -4px; background: #1c2128; border: 2px solid #fde047; border-radius: 10px; padding: 0 5px; font-size: 10px; font-weight: 700; color: #fde047; line-height: 1.4; }
.sglv-ach-user-name { font-size: 14px; font-weight: 700; color: #e6edf3; position: relative; z-index: 1; }
.sglv-ach-user-pts { font-size: 11px; color: #8b949e; position: relative; z-index: 1; }
.sglv-ach-user-pts strong { color: #fbbf24; font-weight: 700; }
/* v2.9.59: 分类导航 — 参考设计图标背景+激活渐变 */
.sglv-ach-nav { padding: 8px 0; flex: 1; }
.sglv-ach-nav-label { font-size: 10px; font-weight: 700; color: #6e7681; text-transform: uppercase; letter-spacing: 0.05em; padding: 8px 16px 4px; }
.sglv-ach-nav-item { display: flex; align-items: center; gap: 10px; padding: 8px 12px; margin: 0 8px; cursor: pointer; transition: all 0.15s; border-radius: 10px; }
.sglv-ach-nav-item:hover { background: rgba(99,102,241,0.08); }
.sglv-ach-nav-item.active { background: linear-gradient(90deg, rgba(99,102,241,0.2), transparent); color: #fff; }
.sglv-ach-nav-item-icon { width: 28px; height: 28px; border-radius: 8px; overflow: hidden; flex-shrink: 0; background: rgba(99,102,241,0.12); display: flex; align-items: center; justify-content: center; }
.sglv-ach-nav-item-icon svg { width: 18px; height: 18px; }
.sglv-ach-nav-item-name { font-size: 12px; font-weight: 600; color: #c9d1d9; flex: 1; }
.sglv-ach-nav-item.active .sglv-ach-nav-item-name { color: #fff; font-weight: 700; }
.sglv-ach-nav-item-badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 6px; flex-shrink: 0; background: rgba(99,102,241,0.15); color: #c7d2fe; }
.sglv-ach-nav-item.active .sglv-ach-nav-item-badge { background: #6366f1; color: #fff; }
.sglv-ach-nav-item-badge.complete { background: rgba(63,185,80,0.15); color: #3fb950; }
.sglv-ach-nav-item-badge.partial { background: rgba(56,139,253,0.15); color: #58a6ff; }
.sglv-ach-nav-item-badge.none { background: rgba(255,255,255,0.06); color: #6e7681; }
/* v2.9.59: 侧边栏最近解锁 — 参考设计mini-icon渐变 */
.sglv-ach-sidebar-recent { padding: 8px 0 12px; border-top: 1px solid rgba(48,54,61,0.4); }
.sglv-ach-sidebar-recent-label { font-size: 10px; font-weight: 700; color: #6e7681; text-transform: uppercase; letter-spacing: 0.05em; padding: 8px 16px 4px; }
.sglv-ach-sidebar-recent-item { display: flex; align-items: center; gap: 10px; padding: 6px 16px; transition: background 0.15s; cursor: pointer; border-radius: 8px; margin: 0 8px; }
.sglv-ach-sidebar-recent-item:hover { background: rgba(99,102,241,0.06); }
.sglv-ach-sidebar-recent-icon { width: 32px; height: 32px; border-radius: 8px; overflow: hidden; flex-shrink: 0; background: linear-gradient(135deg, #6366f1, #a78bfa); display: flex; align-items: center; justify-content: center; }
.sglv-ach-sidebar-recent-icon svg { width: 20px; height: 20px; }
.sglv-ach-sidebar-recent-info { flex: 1; min-width: 0; }
.sglv-ach-sidebar-recent-name { font-size: 11px; font-weight: 600; color: #d2a8ff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sglv-ach-sidebar-recent-time { font-size: 9px; color: #6e7681; margin-top: 1px; }
.sglv-ach-sidebar-recent-empty { font-size: 11px; color: #6e7681; padding: 8px 16px; opacity: 0.6; }
/* ====== 右侧主内容区 ====== */
.sglv-ach-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* v2.9.59: 统计概览区 — 参考设计玻璃态容器 */
.sglv-ach-header { display: flex; align-items: center; gap: 20px; padding: 20px 24px; background: linear-gradient(135deg, rgba(30,41,59,0.6), rgba(15,23,42,0.6)); backdrop-filter: blur(20px); border-bottom: 1px solid rgba(99,102,241,0.12); flex-shrink: 0; }
.sglv-ach-ring-wrap { position: relative; width: 88px; height: 88px; flex-shrink: 0; }
.sglv-ach-ring-svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.sglv-ach-ring-bg { fill: none; stroke: rgba(99,102,241,0.12); stroke-width: 6; }
.sglv-ach-ring-fill { fill: none; stroke: url(#ach-ring-grad); stroke-width: 6; stroke-linecap: round; transition: stroke-dashoffset 0.8s cubic-bezier(0.4,0,0.2,1); }
.sglv-ach-ring-text { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.sglv-ach-ring-val { font-size: 22px; font-weight: 800; background: linear-gradient(135deg, #22d3ee, #a78bfa, #fbbf24); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; line-height: 1; }
.sglv-ach-ring-label { font-size: 9px; color: #64748b; margin-top: 2px; }
/* === v2.9.40: 成就 KPI 卡片复用 sglv-playtime-kpi-card 统一风格 === */
.sglv-ach-kpi-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; flex: 1; min-width: 0; }
.sglv-ach-reset-btn { padding: 8px 16px; border-radius: 10px; border: 1px solid rgba(239,68,68,0.3); background: rgba(239,68,68,0.08); color: #fca5a5; cursor: pointer; font-size: 12px; transition: all 0.2s; flex-shrink: 0; }
.sglv-ach-reset-btn:hover { background: rgba(239,68,68,0.18); border-color: rgba(239,68,68,0.5); }
/* v2.9.98: Steam 平台成就/徽章数横幅 */
.sglv-ach-steam-banner {
display: flex; align-items: center; gap: 12px;
padding: 10px 16px; margin-bottom: 10px;
background: linear-gradient(135deg, rgba(34,211,238,0.08), rgba(251,191,36,0.06), rgba(139,92,246,0.04));
border: 1px solid rgba(34,211,238,0.15);
border-radius: 12px;
position: relative; overflow: hidden;
}
.sglv-ach-steam-banner::after {
content: ''; position: absolute; top: -20px; right: -20px; width: 80px; height: 80px;
background: radial-gradient(circle, rgba(251,191,36,0.12), transparent 60%);
pointer-events: none;
}
.sglv-ach-steam-banner-icon {
width: 36px; height: 36px; border-radius: 10px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, rgba(251,191,36,0.2), rgba(34,211,238,0.12));
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
}
.sglv-ach-steam-banner-icon svg { width: 18px; height: 18px; color: #fbbf24; }
.sglv-ach-steam-banner-info { flex: 1; }
.sglv-ach-steam-banner-label { font-size: 11px; color: #8b949e; font-weight: 600; }
.sglv-ach-steam-banner-val {
font-size: 24px; font-weight: 800; line-height: 1.1;
background: linear-gradient(135deg, #22d3ee, #fbbf24);
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
font-variant-numeric: tabular-nums;
}
.sglv-ach-steam-banner-note { font-size: 10px; color: #6e7681; flex-shrink: 0; }
/* v2.9.59: 成就内容区 — 参考设计容器圆角 */
.sglv-ach-content { flex: 1; padding: 16px 24px; overflow-y: auto; }
.sglv-ach-content-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.sglv-ach-content-title { font-size: 15px; font-weight: 700; color: #e6edf3; }
.sglv-ach-content-count { font-size: 11px; color: #22d3ee; background: rgba(34,211,238,0.1); padding: 3px 10px; border-radius: 8px; font-weight: 600; }
.sglv-ach-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; }
/* v2.9.59: 成就卡片 — 参考设计稀有度左边框+hover提升+图标光泽 */
.sglv-ach-card { display: flex; align-items: flex-start; gap: 14px; padding: 16px 18px; border-radius: 14px; background: linear-gradient(135deg, rgba(15,23,42,0.6), rgba(30,41,59,0.4)); border: 1px solid rgba(99,102,241,0.1); transition: all 0.25s; position: relative; overflow: hidden; }
.sglv-ach-card::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--ach-rarity, #6366f1); opacity: 0.8; }
.sglv-ach-card:hover { transform: translateY(-2px); border-color: rgba(99,102,241,0.3); box-shadow: 0 8px 24px rgba(0,0,0,0.3); }
.sglv-ach-card.unlocked { --ach-rarity: #22c55e; border-color: rgba(34,197,94,0.15); background: linear-gradient(135deg, rgba(34,197,94,0.05), rgba(13,17,23,0.4)); }
.sglv-ach-card.unlocked.rare { --ach-rarity: #22d3ee; }
.sglv-ach-card.unlocked.epic { --ach-rarity: #a78bfa; }
.sglv-ach-card.unlocked.legendary { --ach-rarity: #fbbf24; }
.sglv-ach-card.locked { --ach-rarity: #6366f1; opacity: 0.6; }
.sglv-ach-card.locked.rare { --ach-rarity: #22d3ee; }
.sglv-ach-card.locked.epic { --ach-rarity: #a78bfa; }
.sglv-ach-card.locked.legendary { --ach-rarity: #fbbf24; }
/* v2.9.59: 图标 — 参考设计渐变背景+内阴影+光泽叠加 */
.sglv-ach-card-icon { width: 52px; height: 52px; border-radius: 14px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; overflow: hidden; position: relative; background: linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.08)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.1), inset 0 -1px 0 rgba(0,0,0,0.2); }
.sglv-ach-card-icon::after { content: ''; position: absolute; inset: 0; border-radius: 14px; background: linear-gradient(135deg, rgba(255,255,255,0.12) 0%, transparent 40%); pointer-events: none; }
.sglv-ach-card-icon svg { width: 100%; height: 100%; display: block; }
.sglv-ach-card.unlocked .sglv-ach-card-icon { box-shadow: inset 0 1px 0 rgba(255,255,255,0.15), inset 0 -1px 0 rgba(0,0,0,0.2), 0 4px 12px rgba(99,102,241,0.25); }
.sglv-ach-card.locked .sglv-ach-card-icon { filter: grayscale(70%) brightness(0.5); }
.sglv-ach-card-rarity { position: absolute; top: -2px; right: -2px; width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid #0d1117; z-index: 2; }
.sglv-ach-card-rarity.common { background: #8b949e; }
.sglv-ach-card-rarity.rare { background: #22d3ee; }
.sglv-ach-card-rarity.epic { background: #a78bfa; }
.sglv-ach-card-rarity.legendary { background: #fbbf24; }
.sglv-ach-card-body { flex: 1; min-width: 0; }
.sglv-ach-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 4px; }
.sglv-ach-card-name { font-size: 14px; font-weight: 700; color: #e6edf3; display: flex; align-items: center; gap: 6px; }
.sglv-ach-card.unlocked .sglv-ach-card-name { color: #4ade80; }
/* v2.9.59: NEW 标签 */
.sglv-ach-card-new-tag { font-size: 9px; background: #22c55e; color: #fff; padding: 1px 6px; border-radius: 4px; font-weight: 700; line-height: 1.4; }
/* v2.9.59: 点数徽章 — 参考设计渐变背景 */
.sglv-ach-card-pts { font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 8px; flex-shrink: 0; white-space: nowrap; background: linear-gradient(135deg, rgba(251,191,36,0.15), rgba(245,158,11,0.1)); border: 1px solid rgba(251,191,36,0.3); color: #fbbf24; }
.sglv-ach-card.unlocked .sglv-ach-card-pts { background: rgba(34,197,94,0.1); border-color: rgba(34,197,94,0.3); color: #4ade80; }
.sglv-ach-card-desc { font-size: 11px; color: #94a3b8; line-height: 1.4; margin-bottom: 8px; }
/* v2.9.59: 进度条 — 参考设计微光动画 */
.sglv-ach-card-progress-wrap { display: flex; align-items: center; gap: 8px; }
.sglv-ach-card-progress { flex: 1; height: 6px; background: rgba(34,211,238,0.08); border-radius: 3px; overflow: hidden; position: relative; }
.sglv-ach-card-progress-fill { height: 100%; border-radius: 3px; transition: width 0.6s cubic-bezier(0.4,0,0.2,1); position: relative; }
.sglv-ach-card.unlocked .sglv-ach-card-progress-fill { background: linear-gradient(90deg, #22d3ee, #34d399, #fbbf24); box-shadow: 0 0 8px rgba(34,211,238,0.4); }
.sglv-ach-card.locked .sglv-ach-card-progress-fill { background: linear-gradient(90deg, #38bdf8, #a78bfa, #f59e0b); box-shadow: 0 0 8px rgba(56,189,248,0.25); }
.sglv-ach-card-progress-fill::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); animation: sglv-ach-shimmer 2.5s infinite; }
@keyframes sglv-ach-shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
.sglv-ach-card-progress-val { font-size: 10px; color: #64748b; white-space: nowrap; font-variant-numeric: tabular-nums; }
.sglv-ach-card.unlocked .sglv-ach-card-progress-val { color: #22d3ee; font-weight: 600; }
.sglv-ach-card-unlock-time { font-size: 9px; color: #6e7681; margin-top: 4px; }
.sglv-ach-footer { font-size: 10px; color: #6e7681; text-align: center; padding: 10px 0; margin-top: 8px; border-top: 1px solid rgba(48,54,61,0.3); }`;
const CSS_SHOWCASE =` /* ====== 展柜主布局 ====== */
.sglv-sc-wrap { display: flex; flex-direction: column; height: 100%; min-height: 400px; background: linear-gradient(180deg, #0d1117 0%, #161b22 100%); border-radius: 8px; overflow: hidden; }
.sglv-sc-scroll { flex: 1; overflow-y: auto; padding: 20px; }
/* ====== 英雄区(玩家档案横幅 — 紧凑版 v2) ====== */
.sglv-sc-hero {
position: relative; display: flex; align-items: center; gap: 12px;
padding: 10px 14px; border-radius: 10px; margin-bottom: 14px; overflow: hidden;
background: linear-gradient(135deg, rgba(99,102,241,0.10) 0%, rgba(168,85,247,0.06) 50%, rgba(6,182,212,0.04) 100%);
border: 1px solid rgba(99,102,241,0.15);
}
.sglv-sc-hero::before {
content: ''; position: absolute; inset: 0;
background: radial-gradient(ellipse at top right, rgba(168,85,247,0.10), transparent 60%);
pointer-events: none;
}
.sglv-sc-avatar-wrap { position: relative; flex-shrink: 0; z-index: 1; }
.sglv-sc-avatar {
width: 44px; height: 44px; border-radius: 50%; object-fit: cover;
border: 2px solid rgba(255,255,255,0.12);
box-shadow: 0 1px 8px rgba(0,0,0,0.35), 0 0 0 1px rgba(168,85,247,0.25);
background: linear-gradient(135deg, #1a1a2e, #16213e);
}
.sglv-sc-avatar-placeholder {
width: 44px; height: 44px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: #fff; font-size: 18px; font-weight: 700;
border: 2px solid rgba(255,255,255,0.12);
box-shadow: 0 1px 8px rgba(0,0,0,0.35);
}
.sglv-sc-level-badge {
display: inline-flex; align-items: center;
padding: 1px 7px; border-radius: 8px;
background: linear-gradient(135deg, #f59e0b, #d97706);
color: #fff; font-size: 10px; font-weight: 800;
border: 1px solid rgba(245,158,11,0.4);
box-shadow: 0 1px 4px rgba(245,158,11,0.25);
font-variant-numeric: tabular-nums; line-height: 1.3;
white-space: nowrap; flex-shrink: 0;
}
.sglv-sc-hero-info { flex: 1; min-width: 0; z-index: 1; }
.sglv-sc-hero-name-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.sglv-sc-hero-name { font-size: 15px; font-weight: 800; color: #f1f5f9; line-height: 1.2; text-shadow: 0 1px 3px rgba(0,0,0,0.4); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px; }
.sglv-sc-hero-country {
font-size: 10px; color: #94a3b8; display: inline-flex; align-items: center; gap: 3px;
padding: 1px 6px; border-radius: 8px; background: rgba(148,163,184,0.08);
border: 1px solid rgba(148,163,184,0.1); white-space: nowrap;
}
.sglv-sc-hero-country img { width: 14px; height: 10px; border-radius: 1px; }
.sglv-sc-hero-stats { display: flex; gap: 0; margin-top: 8px; flex-wrap: wrap; }
.sglv-sc-hero-stat { display: flex; align-items: center; gap: 6px; padding: 0 14px; border-right: 1px solid rgba(148,163,184,0.10); }
.sglv-sc-hero-stat:first-child { padding-left: 0; }
.sglv-sc-hero-stat:last-child { border-right: none; }
.sglv-sc-hero-stat svg { width: 14px; height: 14px; color: #818cf8; flex-shrink: 0; }
.sglv-sc-hero-stat-val { font-size: 14px; font-weight: 700; color: #e2e8f0; font-variant-numeric: tabular-nums; line-height: 1; }
.sglv-sc-hero-stat-label { font-size: 10px; color: #64748b; line-height: 1; margin-top: 2px; }
/* ====== 双栏布局 ====== */
.sglv-sc-cols { display: flex; gap: 20px; align-items: flex-start; }
.sglv-sc-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 20px; }
.sglv-sc-right { width: 340px; flex-shrink: 0; }
/* ====== 区块卡片通用 ====== */
.sglv-sc-card {
background: rgba(22,27,34,0.6); border: 1px solid rgba(48,54,61,0.5);
border-radius: 10px; overflow: hidden; backdrop-filter: blur(8px);
}
.sglv-sc-card-header {
display: flex; align-items: center; gap: 8px;
padding: 14px 16px 10px; border-bottom: 1px solid rgba(48,54,61,0.3);
}
.sglv-sc-card-header svg { width: 18px; height: 18px; color: #a78bfa; flex-shrink: 0; }
.sglv-sc-card-title { font-size: 14px; font-weight: 700; color: #e6edf3; }
.sglv-sc-card-body { padding: 14px 16px; }
/* ====== 游戏封面网格(4×2 竖版) ====== */
.sglv-sc-cover-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.sglv-sc-cover-item {
position: relative; aspect-ratio: 2/3; border-radius: 8px; overflow: hidden;
background: #1a1a2e; cursor: pointer;
border: 1px solid rgba(255,255,255,0.06);
transition: border-color 0.2s, box-shadow 0.2s;
}
.sglv-sc-cover-item:hover { border-color: rgba(168,85,247,0.5); box-shadow: 0 4px 16px rgba(168,85,247,0.2); }
.sglv-sc-cover-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sglv-sc-cover-overlay {
position: absolute; inset: 0; display: flex; flex-direction: column;
justify-content: flex-end; padding: 8px;
background: linear-gradient(0deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.2) 50%, transparent 100%);
opacity: 0; transition: opacity 0.2s;
}
.sglv-sc-cover-item:hover .sglv-sc-cover-overlay { opacity: 1; }
.sglv-sc-cover-name { font-size: 11px; font-weight: 600; color: #f1f5f9; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.sglv-sc-cover-hours { font-size: 10px; color: #a5b4fc; margin-top: 2px; font-variant-numeric: tabular-nums; }
.sglv-sc-cover-placeholder {
display: flex; align-items: center; justify-content: center;
width: 100%; height: 100%; color: #475569; font-size: 11px;
}
/* v2.9.99: 骨架屏样式 — shimmer 动画占位块 */
.sglv-sc-sk-block {
background: linear-gradient(90deg, rgba(99,102,241,0.06) 25%, rgba(99,102,241,0.15) 37%, rgba(99,102,241,0.06) 63%);
background-size: 400% 100%;
animation: sglv-sc-shimmer 1.4s ease infinite;
}
@keyframes sglv-sc-shimmer {
0% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.sglv-sc-skeleton-wrap .sglv-sc-playtime-item,
.sglv-sc-skeleton-wrap .sglv-sc-ach-item {
background: transparent;
border-left: none;
}
/* ====== 编辑按钮 ====== */
.sglv-sc-edit-btn {
display: inline-flex; align-items: center; gap: 5px;
padding: 5px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
background: rgba(99,102,241,0.15); border: 1px solid rgba(99,102,241,0.3);
color: #a5b4fc; cursor: pointer; transition: background 0.15s, border-color 0.15s;
margin-left: auto;
}
.sglv-sc-edit-btn:hover { background: rgba(99,102,241,0.25); border-color: rgba(99,102,241,0.5); }
.sglv-sc-edit-btn svg { width: 14px; height: 14px; }
/* ====== 最近游玩时长列表 ====== */
.sglv-sc-playtime-list { display: flex; flex-direction: column; gap: 8px; }
.sglv-sc-playtime-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; background: rgba(13,17,23,0.5); transition: background 0.15s; }
.sglv-sc-playtime-item:hover { background: rgba(99,102,241,0.08); }
.sglv-sc-playtime-icon { width: 32px; height: 32px; border-radius: 6px; object-fit: cover; flex-shrink: 0; background: #1a1a2e; }
.sglv-sc-playtime-icon-ph { width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; background: #1a1a2e; color: #475569; font-size: 10px; flex-shrink: 0; }
.sglv-sc-playtime-info { flex: 1; min-width: 0; }
.sglv-sc-playtime-name { font-size: 12px; font-weight: 600; color: #cbd5e1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sglv-sc-playtime-meta { font-size: 10px; margin-top: 2px; display: flex; align-items: center; gap: 0; }
.sglv-sc-playtime-bar { height: 4px; background: rgba(99,102,241,0.1); border-radius: 2px; margin-top: 4px; overflow: hidden; }
.sglv-sc-playtime-bar-fill { height: 100%; border-radius: 2px; background: linear-gradient(90deg, #6366f1, #a855f7); transition: width 0.6s ease; }
.sglv-sc-playtime-empty { text-align: center; padding: 24px; color: #64748b; font-size: 12px; }
/* ====== 最近解锁成就列表 ====== */
.sglv-sc-ach-list { display: flex; flex-direction: column; gap: 8px; }
.sglv-sc-ach-item { display: flex; align-items: center; gap: 10px; padding: 10px; border-radius: 8px; background: rgba(13,17,23,0.5); border-left: 3px solid transparent; transition: background 0.15s, border-color 0.15s; }
.sglv-sc-ach-item:hover { background: rgba(99,102,241,0.08); }
.sglv-sc-ach-item.rarity-common { border-left-color: #64748b; }
.sglv-sc-ach-item.rarity-rare { border-left-color: #58a6ff; }
.sglv-sc-ach-item.rarity-epic { border-left-color: #a855f7; }
.sglv-sc-ach-item.rarity-legendary { border-left-color: #fbbf24; }
.sglv-sc-ach-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; background: rgba(99,102,241,0.1); }
.sglv-sc-ach-icon svg { width: 22px; height: 22px; }
.sglv-sc-ach-info { flex: 1; min-width: 0; }
.sglv-sc-ach-name { font-size: 12px; font-weight: 600; color: #e2e8f0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sglv-sc-ach-meta { font-size: 10px; color: #64748b; margin-top: 2px; display: flex; align-items: center; gap: 6px; }
.sglv-sc-ach-pts { font-size: 11px; font-weight: 700; color: #fbbf24; flex-shrink: 0; font-variant-numeric: tabular-nums; }
.sglv-sc-ach-empty { text-align: center; padding: 24px; color: #64748b; font-size: 12px; }
/* ====== 玩家画像标签 ====== */
.sglv-sc-tags { display: flex; flex-wrap: wrap; gap: 8px; padding: 14px 16px; }
.sglv-sc-tag {
display: inline-flex; align-items: center; gap: 4px;
padding: 5px 12px; border-radius: 16px; font-size: 11px; font-weight: 600;
border: 1px solid rgba(255,255,255,0.08);
}
.sglv-sc-tag-collector { background: rgba(139,92,246,0.12); border-color: rgba(139,92,246,0.3); color: #c4b5fd; }
.sglv-sc-tag-playtime { background: rgba(6,182,212,0.12); border-color: rgba(6,182,212,0.3); color: #67e8f9; }
.sglv-sc-tag-mastery { background: rgba(16,185,129,0.12); border-color: rgba(16,185,129,0.3); color: #6ee7b7; }
.sglv-sc-tag-diversity { background: rgba(249,115,22,0.12); border-color: rgba(249,115,22,0.3); color: #fdba74; }
.sglv-sc-tag-loyalty { background: rgba(244,114,182,0.12); border-color: rgba(244,114,182,0.3); color: #f9a8d4; }
.sglv-sc-tag-special { background: rgba(251,191,36,0.12); border-color: rgba(251,191,36,0.3); color: #fcd34d; }
/* ====== 封面选择器弹窗(内嵌于游戏库浮窗内,不遮挡浮窗本身) ====== */
.sglv-panel .sglv-sc-picker-overlay {
position: absolute; inset: 0; z-index: 1000002;
background: rgba(0,0,0,0.65); backdrop-filter: blur(4px);
display: flex; align-items: center; justify-content: center;
border-radius: inherit;
animation: sglv-sc-fade-in 0.2s ease;
}
.sglv-sc-picker {
width: 720px; max-width: calc(100% - 40px); max-height: calc(100% - 40px);
background: #161b22; border: 1px solid rgba(48,54,61,0.6);
border-radius: 12px; display: flex; flex-direction: column; overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.sglv-sc-picker-header { display: flex; align-items: center; gap: 10px; padding: 16px 20px; border-bottom: 1px solid rgba(48,54,61,0.4); }
.sglv-sc-picker-title { font-size: 15px; font-weight: 700; color: #e6edf3; flex: 1; }
.sglv-sc-picker-search { width: 200px; padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(48,54,61,0.6); background: #0d1117; color: #e2e8f0; font-size: 12px; outline: none; }
.sglv-sc-picker-search:focus { border-color: rgba(99,102,241,0.5); }
.sglv-sc-picker-close { width: 28px; height: 28px; border-radius: 6px; border: none; background: rgba(255,255,255,0.06); color: #94a3b8; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; }
.sglv-sc-picker-close:hover { background: rgba(255,255,255,0.12); color: #f1f5f9; }
.sglv-sc-picker-body { flex: 1; overflow-y: auto; padding: 16px 20px; overscroll-behavior: contain; }
.sglv-sc-picker-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; }
.sglv-sc-picker-item {
position: relative; aspect-ratio: 2/3; border-radius: 6px; overflow: hidden;
cursor: pointer; border: 2px solid transparent;
transition: border-color 0.15s, transform 0.15s;
}
.sglv-sc-picker-item:hover { transform: scale(1.03); }
.sglv-sc-picker-item.selected { border-color: #22c55e; }
.sglv-sc-picker-item img { width: 100%; height: 100%; object-fit: cover; }
.sglv-sc-picker-item-name { position: absolute; bottom: 0; left: 0; right: 0; padding: 4px 6px; font-size: 9px; color: #fff; background: linear-gradient(0deg, rgba(0,0,0,0.85), transparent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sglv-sc-picker-check { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; border-radius: 50%; background: #22c55e; color: #fff; display: none; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; }
.sglv-sc-picker-item.selected .sglv-sc-picker-check { display: flex; }
.sglv-sc-picker-footer { display: flex; align-items: center; gap: 12px; padding: 12px 20px; border-top: 1px solid rgba(48,54,61,0.4); }
.sglv-sc-picker-count { font-size: 12px; color: #94a3b8; }
.sglv-sc-picker-count strong { color: #22c55e; }
.sglv-sc-picker-save { padding: 7px 18px; border-radius: 6px; border: none; background: linear-gradient(135deg, #6366f1, #8b5cf6); color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; margin-left: auto; transition: opacity 0.15s; }
.sglv-sc-picker-save:hover { opacity: 0.9; }
.sglv-sc-picker-save:disabled { opacity: 0.4; cursor: not-allowed; }
/* ====== v2.9.94: 英雄区徽章展示区 ====== */
.sglv-sc-hero-divider { width: 1px; height: 44px; background: rgba(148,163,184,0.12); flex-shrink: 0; z-index: 1; }
.sglv-sc-hero-badges { display: flex; gap: 8px; align-items: center; flex-shrink: 0; z-index: 1; }
.sglv-sc-hero-badge {
width: 48px; height: 48px; border-radius: 8px;
background: rgba(255,255,255,0.04); border: 1.5px solid rgba(139,92,246,0.3);
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: border-color 0.16s, transform 0.16s, box-shadow 0.16s;
position: relative; overflow: visible;
}
.sglv-sc-hero-badge:hover { border-color: rgba(139,92,246,0.6); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(139,92,246,0.2); }
.sglv-sc-hero-badge img { width: 36px; height: 36px; object-fit: contain; border-radius: 4px; }
.sglv-sc-hero-badge-fallback { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: #818cf8; }
.sglv-sc-hero-badge-fallback svg { width: 22px; height: 22px; }
.sglv-sc-hero-badge-lvl {
position: absolute; bottom: -5px; right: -5px;
font-size: 9px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, #8b5cf6, #6366f1);
border-radius: 4px; padding: 0 4px; line-height: 14px;
box-shadow: 0 1px 3px rgba(0,0,0,0.3); white-space: nowrap;
}
.sglv-sc-hero-badge-foil-tag {
position: absolute; top: -5px; left: -5px;
font-size: 8px; font-weight: 700; color: #1a1a2e;
background: linear-gradient(135deg, #fbbf24, #f59e0b);
border-radius: 3px; padding: 0 3px; line-height: 12px; white-space: nowrap;
}
.sglv-sc-hero-badge-empty {
width: 48px; height: 48px; border-radius: 8px;
border: 1.5px dashed rgba(148,163,184,0.2);
display: flex; align-items: center; justify-content: center;
color: rgba(148,163,184,0.4); font-size: 20px; cursor: pointer;
transition: border-color 0.16s, color 0.16s;
}
.sglv-sc-hero-badge-empty:hover { border-color: rgba(139,92,246,0.4); color: rgba(139,92,246,0.6); }
.sglv-sc-hero-badge-edit {
width: 28px; height: 28px; border-radius: 6px;
background: rgba(139,92,246,0.15); border: 1px solid rgba(139,92,246,0.25);
color: #a78bfa; cursor: pointer; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
transition: background 0.16s, border-color 0.16s;
}
.sglv-sc-hero-badge-edit:hover { background: rgba(139,92,246,0.25); border-color: rgba(139,92,246,0.5); }
.sglv-sc-hero-badge-edit svg { width: 14px; height: 14px; }
/* ====== v2.9.94: 徽章选择器弹窗 ====== */
.sglv-sc-badge-picker { max-width: 680px; }
.sglv-sc-badge-tabs { display: flex; gap: 6px; padding: 10px 20px; border-bottom: 1px solid rgba(48,54,61,0.3); flex-wrap: wrap; }
.sglv-sc-badge-tab {
padding: 5px 12px; border-radius: 6px; border: 1px solid rgba(48,54,61,0.5);
background: transparent; color: #94a3b8; font-size: 12px; font-weight: 600;
cursor: pointer; transition: all 0.16s; white-space: nowrap;
}
.sglv-sc-badge-tab:hover { background: rgba(255,255,255,0.04); color: #c4b5fd; }
.sglv-sc-badge-tab.active { background: rgba(139,92,246,0.15); border-color: rgba(139,92,246,0.4); color: #c4b5fd; }
.sglv-sc-badge-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.sglv-sc-badge-pick-item {
position: relative; border-radius: 8px; overflow: hidden;
background: rgba(22,27,34,0.6); border: 1.5px solid rgba(48,54,61,0.5);
cursor: pointer; transition: border-color 0.16s, transform 0.16s;
display: flex; flex-direction: column; gap: 6px; padding: 8px;
}
.sglv-sc-badge-pick-item:hover { border-color: rgba(139,92,246,0.4); transform: scale(1.02); }
.sglv-sc-badge-pick-item.selected { border-color: #22c55e; }
.sglv-sc-badge-pick-icon {
width: 100%; height: 64px; display: flex; align-items: center; justify-content: center;
background: rgba(13,17,23,0.4); border-radius: 6px; overflow: hidden;
}
.sglv-sc-badge-pick-icon img { max-width: 56px; max-height: 56px; object-fit: contain; }
.sglv-sc-badge-pick-icon.no-icon { }
.sglv-sc-badge-pick-noicon { font-size: 24px; opacity: 0.5; }
.sglv-sc-badge-pick-info { flex: 1; min-width: 0; }
.sglv-sc-badge-pick-name { font-size: 11px; font-weight: 600; color: #e2e8f0; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sglv-sc-badge-pick-meta { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 3px; }
.sglv-sc-badge-pick-meta span { font-size: 9px; padding: 0 4px; border-radius: 3px; line-height: 14px; }
.sglv-sc-badge-foil { background: rgba(245,158,11,0.15); color: #fbbf24; }
.sglv-sc-badge-lvl { background: rgba(139,92,246,0.15); color: #c4b5fd; }
.sglv-sc-badge-pick-xp { background: rgba(34,197,94,0.1); color: #86efac; }
.sglv-sc-badge-pick-item .sglv-sc-picker-check { top: 4px; right: 4px; }
@keyframes sglv-sc-fade-in { from { opacity: 0; } to { opacity: 1; } }
/* ====== 加载/空状态 ====== */
.sglv-sc-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; color: #8a9ba8; }
.sglv-sc-loading svg { animation: sglv-spin 1s linear infinite; }
@media (prefers-reduced-motion: reduce) {
.sglv-sc-cover-overlay, .sglv-sc-cover-item, .sglv-sc-playtime-item, .sglv-sc-ach-item, .sglv-sc-picker-item, .sglv-sc-hero-badge, .sglv-sc-hero-badge-empty, .sglv-sc-badge-pick-item { transition: none !important; }
.sglv-panel .sglv-sc-picker-overlay { animation: none !important; }
.sglv-sc-loading svg { animation: none !important; }
}`;
const CSS_CLOUDSAVE =` /* ====== 云存档主布局 ====== */
.sglv-cs-wrap { display: flex; gap: 0; height: 100%; min-height: 400px; }
.sglv-cs-side { width: 540px; flex-shrink: 0; border-right: 1px solid rgba(255,255,255,0.06); padding: 14px; overflow-y: auto; }
.sglv-cs-list-wrap { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* ====== 状态视图(加载/错误/空) ====== */
.sglv-cs-state-box { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; padding: 40px; text-align: center; width: 100%; }
.sglv-cs-state-icon { font-size: 36px; }
.sglv-cs-state-msg { font-size: 14px; color: var(--sglv-text-1, #e2e8f0); max-width: 360px; line-height: 1.5; }
.sglv-cs-state-tip { font-size: 11px; color: var(--sglv-text-2, #94a3b8); }
.sglv-cs-action-btn { padding: 8px 20px; border-radius: 8px; border: 1px solid rgba(139,92,246,0.4); background: rgba(139,92,246,0.12); color: #c4b5fd; cursor: pointer; font-size: 13px; transition: all 0.2s; }
.sglv-cs-action-btn:hover { background: rgba(139,92,246,0.25); border-color: rgba(139,92,246,0.6); }
/* ====== KPI 卡片(v2.9.36: 炫彩边框 + 色彩变体) ====== */
/* === v2.9.40: 云存档 KPI 卡片复用 sglv-playtime-kpi-card 统一风格(4 列均分) === */
.sglv-cs-kpi-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 18px; }
@media (max-width: 1100px) { .sglv-cs-kpi-row { grid-template-columns: repeat(2, 1fr) !important; } }
/* ====== 仪表盘区块 ====== */
.sglv-cs-section { margin-bottom: 16px; }
.sglv-cs-section-title { font-size: 12px; font-weight: 600; color: var(--sglv-text-1, #e2e8f0); margin-bottom: 8px; }
/* ====== TOP 10 ====== */
.sglv-cs-top10 { display: flex; flex-direction: column; gap: 6px; }
.sglv-cs-top10-item { display: flex; align-items: center; gap: 6px; font-size: 11px; }
.sglv-cs-top10-rank { width: 16px; text-align: center; font-weight: 700; color: var(--sglv-text-2, #94a3b8); flex-shrink: 0; }
.sglv-cs-top10-name { flex-shrink: 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--sglv-text-1, #e2e8f0); width: 140px; }
.sglv-cs-top10-bar-track { flex: 1; height: 8px; background: rgba(255,255,255,0.05); border-radius: 4px; overflow: hidden; min-width: 30px; }
.sglv-cs-top10-bar { height: 100%; background: linear-gradient(90deg, #8b5cf6, #6366f1); border-radius: 4px; transition: width 0.4s ease; }
.sglv-cs-top10-size { flex-shrink: 0; color: var(--sglv-text-2, #94a3b8); font-size: 10px; min-width: 44px; text-align: right; }
/* ====== 分布图 ====== */
/* v2.9.43: 大小分布 + 文件数分布 并排两栏 */
.sglv-cs-dist-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
.sglv-cs-dist-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.05); border-radius: 8px; padding: 10px 8px 8px; }
.sglv-cs-dist-card .sglv-cs-section-title { font-size: 11px; margin-bottom: 6px; text-align: center; }
.sglv-cs-dist { display: flex; align-items: flex-end; gap: 8px; height: 130px; }
.sglv-cs-dist-col { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; justify-content: flex-end; min-width: 0; }
.sglv-cs-dist-bar-track { width: 100%; flex: 1; display: flex; align-items: flex-end; justify-content: center; min-height: 2px; }
.sglv-cs-dist-bar { width: 50%; max-width: 32px; background: linear-gradient(180deg, #8b5cf6, #6366f1); border-radius: 3px 3px 0 0; min-height: 2px; transition: height 0.4s ease; }
.sglv-cs-dist-bar-2 { background: linear-gradient(180deg, #3b82f6, #06b6d4); }
.sglv-cs-dist-val { font-size: 10px; font-weight: 600; color: var(--sglv-text-1, #e2e8f0); margin-top: 3px; }
.sglv-cs-dist-label { font-size: 9px; color: var(--sglv-text-2, #94a3b8); text-align: center; margin-top: 1px; }
/* ====== Diff 徽章 ====== */
.sglv-cs-diff-box { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
.sglv-cs-diff-badge { font-size: 10px; padding: 3px 8px; border-radius: 6px; font-weight: 600; }
.sglv-cs-diff-badge.new { background: rgba(34,197,94,0.15); color: #4ade80; border: 1px solid rgba(34,197,94,0.3); }
.sglv-cs-diff-badge.changed { background: rgba(251,191,36,0.15); color: #fbbf24; border: 1px solid rgba(251,191,36,0.3); }
.sglv-cs-diff-badge.removed { background: rgba(239,68,68,0.15); color: #f87171; border: 1px solid rgba(239,68,68,0.3); }
/* ====== 底部 ====== */
.sglv-cs-footer { display: flex; align-items: center; gap: 8px; margin-top: auto; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.04); flex-wrap: wrap; }
.sglv-cs-source-badge { font-size: 10px; padding: 2px 8px; border-radius: 6px; background: rgba(139,92,246,0.12); color: #c4b5fd; border: 1px solid rgba(139,92,246,0.2); }
.sglv-cs-updated { font-size: 10px; color: var(--sglv-text-2, #94a3b8); }
.sglv-cs-refresh-btn { margin-left: auto; background: none; border: none; color: var(--sglv-text-2, #94a3b8); cursor: pointer; padding: 4px; border-radius: 6px; display: flex; align-items: center; transition: all 0.2s; }
.sglv-cs-refresh-btn:hover { color: #c4b5fd; background: rgba(139,92,246,0.12); }
.sglv-cs-refresh-btn svg { width: 14px; height: 14px; }
/* ====== 工具栏 ====== */
.sglv-cs-toolbar { display: flex; gap: 8px; padding: 10px 12px; border-bottom: 1px solid rgba(255,255,255,0.06); flex-shrink: 0; }
.sglv-cs-search { flex: 1; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 6px 12px; color: var(--sglv-text-1, #e2e8f0); font-size: 12px; outline: none; transition: border-color 0.2s; }
.sglv-cs-search:focus { border-color: rgba(139,92,246,0.5); }
.sglv-cs-search::placeholder { color: var(--sglv-text-2, #94a3b8); }
.sglv-cs-sort { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 6px 8px; color: var(--sglv-text-1, #e2e8f0); font-size: 12px; outline: none; cursor: pointer; }
/* ====== 列表头 ====== */
.sglv-cs-list-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; font-size: 12px; font-weight: 600; color: var(--sglv-text-1, #e2e8f0); flex-shrink: 0; }
.sglv-cs-list-count { font-size: 11px; color: var(--sglv-text-2, #94a3b8); background: rgba(255,255,255,0.05); padding: 2px 8px; border-radius: 6px; }
/* ====== 游戏列表 ====== */
.sglv-cs-game-list { flex: 1; overflow-y: auto; padding: 0 12px 12px; }
.sglv-cs-game-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; transition: background 0.15s; margin-bottom: 4px; }
.sglv-cs-game-row:hover { background: rgba(255,255,255,0.04); }
.sglv-cs-game-cover { width: 46px; height: 22px; border-radius: 4px; object-fit: cover; flex-shrink: 0; background: rgba(255,255,255,0.04); }
/* v2.9.44: 通用 cover failed 样式(sglv-cover-fallback.lib.js 添加) */
.sglv-cs-cover-failed, .sglv-cover-failed { object-fit: contain !important; padding: 4px; opacity: 0.6; }
.sglv-cs-game-cover-fallback { width: 46px; height: 22px; border-radius: 4px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, rgba(59,130,246,0.12), rgba(139,92,246,0.08)); font-size: 9px; color: var(--sglv-text-2, #94a3b8); }
.sglv-cs-game-info { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.sglv-cs-game-name { font-size: 12px; color: var(--sglv-text-1, #e2e8f0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sglv-cs-game-appid { font-size: 10px; color: var(--sglv-text-2, #94a3b8); }
.sglv-cs-game-stats { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.sglv-cs-game-files { font-size: 11px; color: var(--sglv-text-2, #94a3b8); }
.sglv-cs-game-size { font-size: 11px; color: var(--sglv-text-1, #e2e8f0); font-weight: 600; min-width: 60px; text-align: right; }
.sglv-cs-game-dl { font-size: 10px; color: #66c0f4; text-decoration: none; flex-shrink: 0; opacity: 0.7; transition: opacity 0.15s; }
.sglv-cs-game-dl:hover { opacity: 1; text-decoration: underline; }
.sglv-cs-status-badge { font-size: 9px; padding: 2px 6px; border-radius: 4px; font-weight: 700; }
.sglv-cs-status-badge.new { background: rgba(34,197,94,0.15); color: #4ade80; }
.sglv-cs-status-badge.changed { background: rgba(251,191,36,0.15); color: #fbbf24; }
.sglv-cs-list-empty { text-align: center; padding: 40px 20px; font-size: 13px; color: var(--sglv-text-2, #94a3b8); }`;
const CSS_STAGE =` #sglv-stage-progress {
padding: 8px 16px;
background: linear-gradient(180deg, rgba(167,139,250,0.08), transparent);
border-bottom: 1px solid rgba(167,139,250,0.15);
display: block;
}
#sglv-stage-progress .sglv-stage-progress-row {
display: flex;
align-items: center;
gap: 12px;
}
#sglv-stage-progress .sglv-stage-progress-bar {
flex: 1;
height: 6px;
background: rgba(255,255,255,0.06);
border-radius: 3px;
overflow: hidden;
position: relative;
}
#sglv-stage-progress .sglv-stage-progress-fill {
height: 100%;
background: linear-gradient(90deg, #a78bfa 0%, #38bdf8 100%);
width: 0%;
transition: width 0.25s ease-out;
border-radius: 3px;
}
#sglv-stage-progress .sglv-stage-progress-text {
flex-shrink: 0;
font-size: 12px;
color: #94a3b8;
min-width: 200px;
text-align: right;
font-variant-numeric: tabular-nums;
}`;
const CSS_HEADER =` /* === 紧凑页头:删除 h2 标题占用空间,padding 收紧 === */
.sglv-panel > .sglv-header {
padding: 8px 14px !important;
gap: 12px;
cursor: move; /* v2.9.22: 整个 header 作为拖动手柄 */
user-select: none;
}
.sglv-panel > .sglv-header h2 { display: none !important; }
/* === v2.9.22: 保留最左侧 Steam 图标作为品牌标识(去掉"游戏库"文字) === */
.sglv-header-brand {
width: 24px;
height: 24px;
border-radius: 6px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba(102, 192, 244, 0.18), rgba(139, 92, 246, 0.18));
border: 1px solid rgba(102, 192, 244, 0.25);
color: #66c0f4;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
}
.sglv-header-brand:hover { transform: rotate(-8deg) scale(1.05); box-shadow: 0 2px 8px rgba(102, 192, 244, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.1); }
.sglv-header-brand img { width: 14px; height: 14px; display: block; border-radius: 2px; }
.sglv-header-brand svg { width: 14px; height: 14px; }
.sglv-panel > .sglv-header > .sglv-tab-bar {
display: flex;
align-items: center;
gap: 2px;
flex: 1;
min-width: 0;
overflow: visible;
position: relative;
}
/* === tab 按钮紧凑化(v2.9.46: 等宽等距布局,flex:1 使各标签宽度一致) === */
/* v2.9.51: 进一步紧凑——padding 5px 6px → 4px 5px,font-size 12.5px → 11.5px,为右侧全局搜索框腾出空间 */
.sglv-panel .sglv-tab {
flex: 1 1 0 !important;
padding: 4px 5px !important;
font-size: 11.5px !important;
font-weight: 600 !important;
border-radius: 6px !important;
border: 1px solid transparent !important;
background: transparent !important;
color: #94a3b8 !important;
white-space: nowrap !important;
justify-content: center !important;
gap: 4px !important;
min-width: 0 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.15s !important;
height: 26px !important;
}
.sglv-panel .sglv-tab:hover { color: #e2e8f0 !important; background: rgba(255, 255, 255, 0.06) !important; }
.sglv-panel .sglv-tab.active {
color: #fff !important;
background: rgba(102, 192, 244, 0.22) !important;
border-color: rgba(102, 192, 244, 0.4) !important;
box-shadow: 0 0 0 1px rgba(102, 192, 244, 0.15) inset, 0 1px 4px rgba(102, 192, 244, 0.18) !important;
}
.sglv-panel .sglv-tab svg { width: 12px; height: 12px; flex-shrink: 0; }
.sglv-panel .sglv-tab span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* === 头部 action 按钮 hover 反馈 + 状态点 === */
.sglv-panel > .sglv-header > .sglv-header-actions {
gap: 6px;
flex-shrink: 0;
margin-left: auto;
}
.sglv-panel .sglv-header-btn {
width: 30px !important;
height: 30px !important;
}
.sglv-panel .sglv-header-btn.sglv-export-csv-btn,
.sglv-panel .sglv-header-btn.sglv-export-json-btn {
height: 30px !important;
line-height: 30px !important;
}
/* === 头部开关紧凑(家庭组) === */
.sglv-panel .sglv-switch-container {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11.5px;
color: #94a3b8;
user-select: none;
cursor: pointer;
height: 30px;
padding: 0 4px 0 6px;
border-radius: 6px;
transition: background 0.15s, color 0.15s;
}
.sglv-panel .sglv-switch-container:hover { background: rgba(255, 255, 255, 0.05); color: #e2e8f0; }
/* === 头部渐变微调,让 tab active 高光更明显 === */
.sglv-panel > .sglv-header {
background: linear-gradient(135deg, rgba(27, 40, 56, 0.95) 0%, rgba(42, 71, 94, 0.95) 100%) !important;
border-bottom: 1px solid rgba(102, 192, 244, 0.18) !important;
}
/* === v2.9.51: tab 菜单紧凑化(保留),并接入全局搜索框 + 下拉结果浮层 === */
/* 全局搜索框:紧贴 wishlist tab 右侧,flex:0 0 auto 不抢占 tab 等宽空间 */
/* v2.9.51 审查:移除 flex-grow:1(与 flex:0 0 auto 冲突,会抢 tab 空间导致布局错乱) */
.sglv-panel .sglv-global-search {
flex: 0 0 200px !important;
position: relative !important;
display: flex !important;
align-items: center !important;
gap: 4px !important;
height: 26px !important;
padding: 0 6px 0 8px !important;
margin-left: 6px !important;
background: rgba(255, 255, 255, 0.06) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 6px !important;
color: #c7d5e0 !important;
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s, width 0.15s !important;
min-width: 120px !important;
max-width: 240px !important;
}
.sglv-panel .sglv-global-search:hover { background: rgba(255, 255, 255, 0.09) !important; }
.sglv-panel .sglv-global-search:focus-within {
border-color: rgba(102, 192, 244, 0.55) !important;
background: rgba(255, 255, 255, 0.1) !important;
box-shadow: 0 0 0 1px rgba(102, 192, 244, 0.25) !important;
}
.sglv-panel .sglv-global-search-icon {
display: inline-flex !important;
align-items: center !important;
color: #8a9ba8 !important;
flex-shrink: 0 !important;
}
.sglv-panel .sglv-global-search-icon svg { width: 12px !important; height: 12px !important; }
.sglv-panel .sglv-global-search input {
flex: 1 1 auto !important;
min-width: 0 !important;
border: none !important;
background: transparent !important;
color: #fff !important;
font-size: 11.5px !important;
line-height: 1 !important;
outline: none !important;
padding: 0 !important;
height: 100% !important;
}
.sglv-panel .sglv-global-search input::placeholder { color: #6b7c8c !important; font-size: 11px !important; }
.sglv-panel .sglv-global-search-clear {
flex: 0 0 auto !important;
display: none !important;
align-items: center !important;
justify-content: center !important;
width: 16px !important;
height: 16px !important;
padding: 0 !important;
border: none !important;
border-radius: 4px !important;
background: rgba(255, 255, 255, 0.1) !important;
color: #c7d5e0 !important;
cursor: pointer !important;
transition: background 0.15s, color 0.15s !important;
}
.sglv-panel .sglv-global-search-clear:hover { background: rgba(255, 255, 255, 0.2) !important; color: #fff !important; }
.sglv-panel .sglv-global-search-clear svg { width: 9px !important; height: 9px !important; }
.sglv-panel .sglv-global-search.has-text .sglv-global-search-clear { display: inline-flex !important; }
/* v2.9.58: 搜索结果浮层 — 右对齐搜索框,动画显隐,动态 max-height 防溢出面板 */
.sglv-panel .sglv-global-search-pop {
position: absolute !important;
top: calc(100% + 6px) !important;
right: 0 !important;
left: auto !important;
min-width: 300px !important;
max-width: 400px !important;
max-height: calc(80vh - 80px) !important;
overflow-y: auto !important;
background: rgba(15, 23, 42, 0.98) !important;
border: 1px solid rgba(102, 192, 244, 0.35) !important;
border-radius: 10px !important;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.3) !important;
z-index: 1000001 !important;
padding: 4px 0 !important;
backdrop-filter: blur(12px) !important;
display: block !important;
opacity: 0;
transform: translateY(-6px) scale(0.98);
visibility: hidden;
transition: opacity 0.16s ease, transform 0.16s ease, visibility 0s linear 0.16s;
transform-origin: top right;
}
.sglv-panel .sglv-global-search-pop::-webkit-scrollbar { width: 6px; }
.sglv-panel .sglv-global-search-pop::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); border-radius: 3px; }
.sglv-panel .sglv-global-search-pop.show {
opacity: 1;
transform: translateY(0) scale(1);
visibility: visible;
transition: opacity 0.16s ease, transform 0.16s ease, visibility 0s;
}
.sglv-panel .sglv-gs-header {
padding: 8px 12px 5px !important;
font-size: 10px !important;
font-weight: 700 !important;
color: #6e7681 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.04) !important;
}
.sglv-panel .sglv-gs-header-count {
background: rgba(102, 192, 244, 0.12) !important;
color: #66c0f4 !important;
font-weight: 700 !important;
text-transform: none !important;
letter-spacing: 0 !important;
padding: 1px 7px !important;
border-radius: 10px !important;
font-size: 10px !important;
}
.sglv-panel .sglv-gs-item {
display: flex !important;
align-items: center !important;
gap: 10px !important;
padding: 7px 12px !important;
cursor: pointer !important;
color: #c7d5e0 !important;
font-size: 12px !important;
transition: background 0.12s, color 0.12s !important;
border-left: 2px solid transparent !important;
}
.sglv-panel .sglv-gs-item:hover,
.sglv-panel .sglv-gs-item.focus {
background: rgba(102, 192, 244, 0.12) !important;
color: #fff !important;
border-left-color: rgba(102, 192, 244, 0.6) !important;
}
.sglv-panel .sglv-gs-item-thumb {
width: 48px !important;
height: 18px !important;
flex-shrink: 0 !important;
background: rgba(255, 255, 255, 0.04) !important;
border-radius: 3px !important;
overflow: hidden !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: transform 0.15s ease !important;
}
.sglv-panel .sglv-gs-item:hover .sglv-gs-item-thumb,
.sglv-panel .sglv-gs-item.focus .sglv-gs-item-thumb {
transform: scale(1.08) !important;
}
.sglv-panel .sglv-gs-item-thumb img { width: 100% !important; height: 100% !important; object-fit: cover !important; }
.sglv-panel .sglv-gs-item-body { flex: 1 1 auto !important; min-width: 0 !important; }
.sglv-panel .sglv-gs-item-name {
font-size: 12px !important;
font-weight: 600 !important;
color: #e2e8f0 !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.sglv-panel .sglv-gs-item.focus .sglv-gs-item-name { color: #fff !important; }
.sglv-panel .sglv-gs-item-name mark {
background: rgba(102, 192, 244, 0.35) !important;
color: #fff !important;
padding: 0 1px !important;
border-radius: 2px !important;
}
.sglv-panel .sglv-gs-item-sub {
font-size: 10px !important;
color: #8a9ba8 !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
margin-top: 1px !important;
}
/* v2.9.58: 右侧徽章+图标容器,统一收紧间距 */
.sglv-panel .sglv-gs-item-right {
display: flex !important;
align-items: center !important;
gap: 4px !important;
flex-shrink: 0 !important;
}
.sglv-panel .sglv-gs-item-type {
flex-shrink: 0 !important;
font-size: 9px !important;
font-weight: 700 !important;
padding: 1px 5px !important;
border-radius: 3px !important;
text-transform: uppercase !important;
letter-spacing: 0.04em !important;
}
.sglv-panel .sglv-gs-item-type.owned { background: rgba(63, 185, 80, 0.15) !important; color: #3fb950 !important; }
.sglv-panel .sglv-gs-item-type.wishlist { background: rgba(245, 158, 11, 0.15) !important; color: #f59e0b !important; }
.sglv-panel .sglv-gs-item-type.store { background: rgba(102, 192, 244, 0.15) !important; color: #66c0f4 !important; }
/* v2.9.83: 精准匹配(AppID直接命中)行高亮 */
.sglv-panel .sglv-gs-item-exact { border-left: 3px solid #4ade80 !important; background: rgba(74, 222, 128, 0.04) !important; }
.sglv-panel .sglv-gs-item-exact:hover, .sglv-panel .sglv-gs-item-exact.focus { background: rgba(74, 222, 128, 0.08) !important; }
/* v2.9.53: 家庭共享次级徽章(紫色,小尺寸,紧贴主 type 徽章右侧) */
.sglv-panel .sglv-gs-item-shared {
display: inline-flex !important; align-items: center !important;
padding: 1px 6px !important; margin-left: 0 !important;
font-size: 10px !important; font-weight: 600 !important;
border-radius: 3px !important;
background: rgba(139, 92, 246, 0.18) !important;
color: #c4b5fd !important;
border: 1px solid rgba(139, 92, 246, 0.35) !important;
cursor: help !important;
}
.sglv-panel .sglv-gs-item-ext { display: inline-flex !important; align-items: center !important; color: #6e7681 !important; }
.sglv-panel .sglv-gs-item-ext svg { width: 10px !important; height: 10px !important; }
.sglv-panel .sglv-gs-empty {
padding: 20px 12px !important;
text-align: center !important;
color: #6e7681 !important;
font-size: 11px !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
gap: 8px !important;
}
.sglv-panel .sglv-gs-empty-icon { opacity: 0.4 !important; }
.sglv-panel .sglv-gs-empty-icon svg { width: 28px !important; height: 28px !important; }
.sglv-panel .sglv-gs-footer {
padding: 6px 12px !important;
border-top: 1px solid rgba(255, 255, 255, 0.05) !important;
font-size: 10px !important;
color: #6e7681 !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
background: rgba(15, 23, 42, 0.5) !important;
}
.sglv-panel .sglv-gs-footer-hint {
display: flex !important;
align-items: center !important;
gap: 3px !important;
}
.sglv-panel .sglv-gs-footer kbd {
background: rgba(255, 255, 255, 0.08) !important;
border: 1px solid rgba(255, 255, 255, 0.12) !important;
border-radius: 3px !important;
padding: 0 4px !important;
font-size: 9px !important;
font-family: monospace !important;
color: #c7d5e0 !important;
}
/* === v2.9.63: 游戏详情浮窗(点击搜索结果后弹出) === */
.sglv-panel .sglv-detail-overlay {
position: absolute; inset: 0; background: rgba(0,0,0,0.5);
z-index: 1000002; border-radius: inherit;
opacity: 0; visibility: hidden;
transition: opacity 0.16s ease, visibility 0s linear 0.16s;
}
.sglv-panel .sglv-detail-overlay.show { opacity: 1; visibility: visible; transition: opacity 0.16s ease, visibility 0s; }
.sglv-panel .sglv-detail-pop {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -48%) scale(0.96);
width: calc(100% - 48px); max-width: 560px; max-height: calc(85vh - 60px);
background: linear-gradient(160deg, rgba(23,33,45,0.99), rgba(15,23,42,0.99));
border: 1px solid rgba(102,192,244,0.25); border-radius: 14px;
box-shadow: 0 20px 60px rgba(0,0,0,0.6), 0 0 0 1px rgba(0,0,0,0.3);
z-index: 1000003; overflow: hidden; display: flex; flex-direction: column;
opacity: 0; visibility: hidden;
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
}
.sglv-panel .sglv-detail-pop.show {
opacity: 1; visibility: visible; transform: translate(-50%, -50%) scale(1);
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s;
}
.sglv-detail-close {
position: absolute; top: 10px; right: 10px; z-index: 5;
width: 30px; height: 30px; border-radius: 50%; border: none;
background: rgba(0,0,0,0.4); color: #c7d5e0; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.16s, color 0.16s;
}
.sglv-detail-close:hover { background: rgba(244,67,54,0.5); color: #fff; }
.sglv-detail-close svg { width: 16px; height: 16px; }
.sglv-detail-scroll { overflow-y: auto; overflow-x: hidden; }
.sglv-detail-scroll::-webkit-scrollbar { width: 6px; }
.sglv-detail-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
.sglv-detail-hero { position: relative; width: 100%; aspect-ratio: 231/87; overflow: hidden; background: #1a2332; }
.sglv-detail-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sglv-detail-hero::after { content:''; position:absolute; inset:0; background: linear-gradient(180deg, transparent 40%, rgba(15,23,42,0.95)); }
.sglv-detail-body { padding: 0 18px 18px; }
.sglv-detail-name { font-size: 18px; font-weight: 700; color: #e2e8f0; margin: -20px 0 4px; position: relative; z-index: 1; text-shadow: 0 2px 8px rgba(0,0,0,0.8); }
.sglv-detail-desc { font-size: 12px; color: #8b949e; line-height: 1.6; margin: 10px 0; }
.sglv-detail-meta { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 11px; margin: 10px 0; }
.sglv-detail-meta dt { color: #6e7681; font-weight: 600; }
.sglv-detail-meta dd { color: #c7d5e0; margin: 0; }
.sglv-detail-tags { display: flex; flex-wrap: wrap; gap: 4px; margin: 10px 0; }
.sglv-detail-tag { font-size: 10px; padding: 2px 8px; border-radius: 4px; background: rgba(102,192,244,0.1); color: #66c0f4; border: 1px solid rgba(102,192,244,0.15); }
.sglv-detail-price { display: flex; align-items: center; gap: 8px; margin: 10px 0; }
.sglv-detail-price-val { font-size: 16px; font-weight: 700; color: #4ade80; }
.sglv-detail-price-discount { font-size: 12px; color: #fbbf24; }
.sglv-detail-price-free { font-size: 16px; font-weight: 700; color: #4ade80; }
.sglv-detail-shots { display: flex; gap: 6px; overflow-x: auto; margin: 10px 0; padding-bottom: 4px; }
.sglv-detail-shots::-webkit-scrollbar { height: 4px; }
.sglv-detail-shots::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 2px; }
.sglv-detail-shot { flex-shrink: 0; width: 200px; height: 113px; border-radius: 6px; overflow: hidden; background: #1a2332; }
.sglv-detail-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sglv-detail-actions { display: flex; gap: 8px; margin-top: 14px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,0.06); }
.sglv-detail-btn { display: inline-flex; align-items: center; gap: 5px; padding: 8px 16px; border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer; border: none; transition: all 0.16s; text-decoration: none; }
.sglv-detail-btn-primary { background: linear-gradient(135deg, #66c0f4, #1a9fff); color: #fff; }
.sglv-detail-btn-primary:hover { filter: brightness(1.1); }
.sglv-detail-btn-secondary { background: rgba(255,255,255,0.08); color: #c7d5e0; }
.sglv-detail-btn-secondary:hover { background: rgba(255,255,255,0.14); }
.sglv-detail-btn-ai { background: linear-gradient(135deg, #6366f1, #8b5cf6); color: #fff; }
.sglv-detail-btn-ai:hover { filter: brightness(1.15); }
.sglv-detail-btn svg { width: 14px; height: 14px; }
.sglv-detail-loading { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px; gap: 12px; color: #6e7681; font-size: 12px; }
.sglv-detail-error { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px; gap: 8px; color: #f87171; font-size: 12px; }
.sglv-detail-metacritic { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; padding: 2px 8px; border-radius: 4px; font-weight: 700; }
/* === v2.9.22: body 首屏骨架占位(避免空数据时出现完全空白) === */
.sglv-body-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 320px;
padding: 40px 20px;
gap: 16px;
color: #94a3b8;
font-size: 13px;
text-align: center;
}
.sglv-body-loading .sglv-bl-title {
font-size: 15px;
font-weight: 600;
color: #cbd5e1;
}
.sglv-body-loading .sglv-bl-tip {
font-size: 12px;
color: #64748b;
line-height: 1.6;
max-width: 420px;
}
.sglv-body-loading .sglv-bl-spinner {
width: 36px;
height: 36px;
border: 3px solid rgba(102, 192, 244, 0.15);
border-top-color: #66c0f4;
border-radius: 50%;
animation: sglv-spin 0.9s linear infinite;
}
.sglv-body-loading .sglv-bl-actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
.sglv-body-loading .sglv-bl-btn {
padding: 6px 14px;
font-size: 12px;
font-weight: 600;
border-radius: 6px;
border: 1px solid rgba(102, 192, 244, 0.3);
background: rgba(102, 192, 244, 0.1);
color: #66c0f4;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.sglv-body-loading .sglv-bl-btn:hover {
background: rgba(102, 192, 244, 0.2);
border-color: rgba(102, 192, 244, 0.5);
}
.sglv-body-loading .sglv-bl-btn.primary {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.25), rgba(102, 192, 244, 0.2));
border-color: rgba(139, 92, 246, 0.4);
color: #c4b5fd;
}
.sglv-body-loading .sglv-bl-btn.primary:hover {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.35), rgba(102, 192, 244, 0.3));
}
/* === v2.9.26: 统计仪表布局——热力图行改自适应内容高度(不再撑满 wrap),6月增量SVG加高减少留白 === */
/* v2.9.27: sglv-body 需 min-height:0 确保 flex 子项正确滚动(移动端关键修复) */
.sglv-body { min-height: 0 !important; }
/* wrap: flex:1 填满 sglv-body 可用高度,内部 flex column 纵向排列各区块 */
.sglv-playtime-wrap {
display: flex !important;
flex-direction: column !important;
flex: 1 1 0 !important;
min-height: 0 !important;
gap: 10px !important;
overflow-y: auto !important;
padding: 12px !important;
}
/* v2.9.55: 图表行(甜甜圈+趋势) 使用内容高度,不再 flex-grow 撑满 wrap 剩余空间。
低分屏下 flex-grow:1 会吞掉热力图行的可用高度,导致下方热力图/增量图被挤压变形。
改为 flex:0 0 auto 后,各行按内容自然排列,wrap 的 overflow-y:auto 负责纵向滚动。 */
.sglv-playtime-row { flex: 0 0 auto !important; min-height: 0 !important; }
/* v2.9.55: 图表列最小高度,确保甜甜圈/趋势图有足够渲染空间 */
.sglv-playtime-col { min-height: 220px !important; }
/* v2.9.26: 热力图行 grid 7:3——改成自适应内容高度 + 最小高度,不再撑满 wrap 剩余高度,避免盖住上方 KPI/图表区 */
/* v2.9.55: overflow 从 hidden 改为 visible,允许内容自然溢出由 wrap 的 overflow-y:auto 滚动,而非裁剪变形 */
.sglv-heatmap-acquire-row {
display: grid !important;
grid-template-columns: 7fr 3fr !important;
align-items: stretch !important;
min-height: 320px !important;
flex: 0 0 auto !important;
gap: 10px !important;
overflow: visible !important;
}
.sglv-heatmap-acquire-row .sglv-heatmap-section,
.sglv-heatmap-acquire-row .sglv-heatmap-mini-section {
flex: none !important;
min-width: 0 !important;
max-width: none !important;
min-height: 280px !important;
display: flex !important;
flex-direction: column !important;
overflow: visible !important;
}
/* v2.9.26: 热力图 block 撑满 section 高度 */
.sglv-heatmap-acquire-row .sglv-heatmap-block,
.sglv-heatmap-acquire-row .sglv-heatmap-mini-skeleton,
.sglv-heatmap-acquire-row .sglv-hm-mini-inner {
flex: 1 1 0 !important;
min-height: 0 !important;
}
/* v2.9.55: 增量柱图——SVG 用 auto 高度而非 100%,避免容器被压缩时 SVG 跟着变形 */
.sglv-heatmap-acquire-row .sglv-hm-mini-chart { flex: 1 1 auto !important; min-height: 120px !important; }
.sglv-heatmap-acquire-row .sglv-hm-mini-chart svg { height: auto !important; max-height: 100% !important; }
/* v2.9.26: 热力图 SVG 保持原始尺寸,由 scroll 容器水平滚动展示完整内容 */
.sglv-heatmap-block svg {
max-width: 100% !important;
height: auto !important;
}
.sglv-heatmap-scroll svg {
max-width: none !important;
height: auto !important;
display: block !important;
}
.sglv-heatmap-acquire-row .sglv-heatmap-scroll { overflow-x: auto !important; }
/* v2.9.26: 热力图 section 紧凑 padding + scroll 容器居中(SVG 内容垂直居中) */
.sglv-heatmap-acquire-row .sglv-heatmap-section { padding: 10px 12px 8px !important; }
.sglv-heatmap-acquire-row .sglv-heatmap-mini-section { padding: 10px 12px 8px !important; }
.sglv-heatmap-section > #sglv-pt-heatmap {
flex: 1 1 0 !important;
min-height: 0 !important;
display: flex !important;
flex-direction: column !important;
justify-content: flex-start !important;
}
/* v2.9.26: 热力图 scroll 紧凑 padding-bottom */
.sglv-heatmap-scroll { padding-bottom: 2px !important; }
/* v2.9.26: heatmap-body 擑满 block 剩余高度,scroll 区域可滚动 + 居中 SVG */
.sglv-heatmap-acquire-row .sglv-heatmap-body { flex: 1 1 0 !important; min-height: 0 !important; display: flex !important; flex-direction: column !important; }
.sglv-heatmap-acquire-row .sglv-heatmap-body .sglv-heatmap-scroll { flex: 1 1 0 !important; min-height: 0 !important; display: flex !important; flex-direction: column !important; justify-content: center !important; }
.sglv-heatmap-acquire-row .sglv-heatmap-body .sglv-heatmap-scroll > svg { flex: 0 0 auto !important; align-self: flex-start !important; }
/* 趋势图折线等比缩放(meet)后居中 */
.sglv-pt-trend-chart { align-items: center !important; }
/* v2.9.23: 增量柱图底部统计栏样式 */
.sglv-hm-mini-summary { display: flex; flex-wrap: wrap; gap: 8px; padding: 6px 4px 0; flex-shrink: 0; }
.sglv-hm-mini-stat { font-size: 10px; color: var(--sglv-text-secondary); white-space: nowrap; }
.sglv-hm-mini-stat b { font-size: 13px; font-weight: 800; color: #e0e0e0; }
/* 移动端热力图行恢复上下排列 */
@media (max-width: 900px) {
.sglv-heatmap-acquire-row { grid-template-columns: 1fr !important; }
}
/* === v2.9.27: 移动端统计仪表适配——KPI 卡片防换行 + 热力图高度收缩 === */
/* KPI 卡片内部文字截断,防止窄屏换行变形 */
.sglv-playtime-kpi-value { white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; }
.sglv-playtime-kpi-sub { white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; }
.sglv-playtime-kpi-label { white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; }
@media (max-width: 768px) {
/* KPI 卡片:2 列布局时进一步紧凑化 */
.sglv-playtime-kpi-row { gap: 5px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-card { padding: 5px 8px 4px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-value { font-size: 15px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-label { font-size: 9px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-sub { font-size: 8px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-icon { width: 14px !important; height: 14px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-icon svg { width: 9px !important; height: 9px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-unit { font-size: 10px !important; }
/* v2.9.55: 热力图行降低最小高度,但仍保持足够渲染空间,由 wrap 滚动而非压缩 */
.sglv-heatmap-acquire-row { min-height: 260px !important; }
.sglv-heatmap-acquire-row .sglv-heatmap-section,
.sglv-heatmap-acquire-row .sglv-heatmap-mini-section { padding: 8px 10px 6px !important; min-height: 220px !important; }
/* 图表行(甜甜圈+趋势) 单列堆叠时限制高度,避免无限增高 */
.sglv-playtime-row { flex: 0 0 auto !important; }
.sglv-playtime-col { min-height: 200px !important; max-height: 280px !important; }
.sglv-playtime-col > #sglv-pt-donut { min-height: 160px !important; }
.sglv-pt-trend-chart { min-height: 160px !important; }
}
@media (max-width: 480px) {
/* 超窄屏:KPI 保持 2 列但进一步缩小 */
.sglv-playtime-kpi-row { gap: 4px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-card { padding: 4px 6px 3px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-value { font-size: 14px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-label { font-size: 8px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-sub { font-size: 7px !important; }
/* v2.9.55: 热力图行降低高度但保持最小渲染空间 */
.sglv-heatmap-acquire-row { min-height: 220px !important; }
.sglv-heatmap-acquire-row .sglv-heatmap-section,
.sglv-heatmap-acquire-row .sglv-heatmap-mini-section { min-height: 180px !important; }
/* 图表列限制高度 */
.sglv-playtime-col { min-height: 180px !important; max-height: 240px !important; }
/* wrap 内边距收紧 */
.sglv-playtime-wrap { padding: 8px !important; gap: 8px !important; }
}
/* === v2.9.25: 分页导航——简化单行布局,首页|上页|下页|末页 + 跳页输入 === */
.sglv-tl-pagination,
.sglv-pagination,
.sglv-pt-pagination {
display: flex !important;
align-items: center !important;
justify-content: center !important;
flex-wrap: nowrap !important;
gap: 4px !important;
padding: 8px 0 2px !important;
overflow: hidden !important;
}
/* 箭头/文本按钮 */
.sglv-page-arrow {
min-width: 24px; height: 24px; padding: 0 2px;
border: 1px solid rgba(255,255,255,0.1); border-radius: 5px;
background: rgba(255,255,255,0.04); color: #c7d5e0;
font-size: 13px; line-height: 1; cursor: pointer;
transition: var(--sglv-transition); text-align: center; flex-shrink: 0;
}
.sglv-page-arrow:hover:not(:disabled) { background: rgba(102,192,244,0.15); border-color: rgba(102,192,244,0.4); color: #fff; }
.sglv-page-arrow:disabled { opacity: 0.3; cursor: not-allowed; }
/* 中文文本按钮——加宽适配文字 */
.sglv-page-text-btn { min-width: auto !important; padding: 0 8px !important; font-size: 11px !important; white-space: nowrap !important; }
/* 页码按钮组(已废弃,隐藏) */
.sglv-page-btns { display: none !important; }
.sglv-page-btn { display: none !important; }
.sglv-page-ellipsis { display: none !important; }
/* 跳页输入框——紧凑,回车跳转 */
.sglv-page-jump-input {
width: 38px; height: 24px; padding: 0 2px; text-align: center;
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1); border-radius: 4px;
color: #fff; font-size: 11px; outline: none; flex-shrink: 0;
-moz-appearance: textfield;
}
.sglv-page-jump-input::-webkit-inner-spin-button,
.sglv-page-jump-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.sglv-page-jump-input:focus { border-color: rgba(102,192,244,0.5); }
/* 页码信息——极简 */
.sglv-page-info { font-size: 10px !important; color: var(--sglv-text-secondary) !important; white-space: nowrap !important; flex-shrink: 0 !important; }
/* 工具栏内嵌分页——更紧凑,隐藏箭头和跳页 */
.sglv-pagination-inline .sglv-page-arrow { min-width: 20px; height: 20px; font-size: 11px; }
.sglv-pagination-inline .sglv-page-btn { min-width: 20px; height: 20px; font-size: 10px; }
.sglv-pagination-inline .sglv-page-jump-input { display: none; }
.sglv-pagination-inline .sglv-page-info { display: none; }
/* === v2.9.23: 统计仪表 KPI 卡片 6 列 1 行紧凑布局 + 扁平化(参考设计图) === */
.sglv-playtime-kpi-row {
grid-template-columns: repeat(6, 1fr) !important;
gap: 6px !important;
}
@media (max-width: 1280px) { .sglv-playtime-kpi-row { grid-template-columns: repeat(3, 1fr) !important; } }
@media (max-width: 700px) { .sglv-playtime-kpi-row { grid-template-columns: repeat(2, 1fr) !important; } }
/* 6 列 1 行时各元素字号紧凑 */
.sglv-playtime-kpi-row .sglv-playtime-kpi-value { font-size: 17px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-label { font-size: 10px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-sub { font-size: 9px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-icon { width: 16px !important; height: 16px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-icon svg { width: 10px !important; height: 10px !important; }
.sglv-playtime-kpi-row .sglv-playtime-kpi-unit { font-size: 11px !important; }
/* v2.9.23: KPI 卡片扁平化——弱化边框,靠左侧色条 + 背景色区分,参考设计图 */
.sglv-playtime-kpi-card { border-width: 1px !important; border-left-width: 3px !important; }
/* === v2.9.22: 游玩率/完成度 KPI 卡片颜色变体(cyan/rose)+ % 单位字号 === */
.sglv-playtime-kpi-card.cyan { border-color: rgba(34, 211, 238, 0.35); }
.sglv-playtime-kpi-card.cyan::before { background: #22d3ee; }
.sglv-playtime-kpi-card.cyan .sglv-playtime-kpi-value { color: #22d3ee; }
.sglv-playtime-kpi-card.cyan .sglv-playtime-kpi-icon { background: rgba(34, 211, 238, 0.15); color: #22d3ee; }
.sglv-playtime-kpi-card.rose { border-color: rgba(244, 63, 94, 0.35); }
.sglv-playtime-kpi-card.rose::before { background: #f43f5e; }
.sglv-playtime-kpi-card.rose .sglv-playtime-kpi-value { color: #fb7185; }
.sglv-playtime-kpi-card.rose .sglv-playtime-kpi-icon { background: rgba(244, 63, 94, 0.15); color: #fb7185; }
.sglv-playtime-kpi-card.gold { border-color: rgba(251, 191, 36, 0.3); }
.sglv-playtime-kpi-card.gold::before { background: #fbbf24; }
.sglv-playtime-kpi-card.gold .sglv-playtime-kpi-value { color: #fbbf24; }
.sglv-playtime-kpi-card.gold .sglv-playtime-kpi-icon { background: rgba(251, 191, 36, 0.12); color: #fbbf24; }
.sglv-playtime-kpi-unit { font-size: 13px; font-weight: 700; opacity: 0.85; margin-left: 2px; }
/* === v2.9.22: 入库趋势页 KPI 紧凑布局(4 卡放左半侧顶部)+ 时间线撑到顶部 === */
.sglv-trend-kpi-row-compact {
gap: 6px !important;
margin-bottom: 8px !important;
}
.sglv-trend-kpi-row-compact .sglv-trend-kpi-card { padding: 7px 10px !important; }
.sglv-trend-kpi-row-compact .sglv-trend-kpi-value { font-size: 17px !important; }
.sglv-trend-kpi-row-compact .sglv-trend-kpi-label { font-size: 10px !important; }
.sglv-trend-kpi-row-compact .sglv-trend-kpi-sub { font-size: 9px !important; }
.sglv-trend-kpi-row-compact .sglv-trend-kpi-icon { width: 16px !important; height: 16px !important; }
.sglv-trend-kpi-row-compact .sglv-trend-kpi-icon svg { width: 10px !important; height: 10px !important; }
/* 时间线顶到页面顶部:rightCol 整体高度撑满 */
.sglv-trend-split-right { align-self: stretch; }
/* === v2.9.48: KPI 两行布局 + 新色彩变体 === */
.sglv-trend-kpi-wrap { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; flex-shrink: 0; }
.sglv-trend-kpi-wrap .sglv-trend-kpi-row { margin-bottom: 0 !important; }
.sglv-trend-kpi-card.green { border-color: rgba(52,211,153,0.3); }
.sglv-trend-kpi-card.green::before { background: #34d399; }
.sglv-trend-kpi-card.green .sglv-trend-kpi-value { color: #34d399; }
.sglv-trend-kpi-card.green .sglv-trend-kpi-icon { background: rgba(52,211,153,0.15); color: #34d399; }
.sglv-trend-kpi-card.orange { border-color: rgba(245,158,11,0.3); }
.sglv-trend-kpi-card.orange::before { background: #f59e0b; }
.sglv-trend-kpi-card.orange .sglv-trend-kpi-value { color: #f59e0b; }
.sglv-trend-kpi-card.orange .sglv-trend-kpi-icon { background: rgba(245,158,11,0.15); color: #f59e0b; }
.sglv-trend-kpi-card.cyan { border-color: rgba(6,182,212,0.3); }
.sglv-trend-kpi-card.cyan::before { background: #06b6d4; }
.sglv-trend-kpi-card.cyan .sglv-trend-kpi-value { color: #22d3ee; }
.sglv-trend-kpi-card.cyan .sglv-trend-kpi-icon { background: rgba(6,182,212,0.15); color: #22d3ee; }
.sglv-trend-kpi-card.purple { border-color: rgba(167,139,250,0.3); }
.sglv-trend-kpi-card.purple::before { background: #a78bfa; }
.sglv-trend-kpi-card.purple .sglv-trend-kpi-value { color: #a78bfa; }
.sglv-trend-kpi-card.purple .sglv-trend-kpi-icon { background: rgba(167,139,250,0.15); color: #a78bfa; }
.sglv-trend-kpi-unit { font-size: 10px; font-weight: 600; opacity: 0.7; margin-left: 2px; }
/* === v2.9.48: 右侧标签切换 === */
.sglv-trend-right-tabs { display: flex; gap: 2px; margin-bottom: 10px; flex-shrink: 0; border-bottom: 1px solid rgba(148,163,184,0.12); }
.sglv-trend-right-tab { display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px; font-size: 12px; font-weight: 600; color: #8a9ba8; background: transparent; border: none; border-bottom: 2px solid transparent; cursor: pointer; transition: color 0.15s, border-color 0.15s; white-space: nowrap; }
.sglv-trend-right-tab svg { width: 14px; height: 14px; }
.sglv-trend-right-tab:hover { color: #cbd5e1; }
.sglv-trend-right-tab.active { color: var(--sglv-accent, #66c0f4); border-bottom-color: var(--sglv-accent, #66c0f4); }
.sglv-trend-right-content { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
/* === v2.9.48: 频率分析图表 === */
.sglv-freq-scroll { flex: 1; overflow-y: auto; min-height: 0; padding-right: 2px; }
.sglv-freq-scroll::-webkit-scrollbar { width: 5px; }
.sglv-freq-scroll::-webkit-scrollbar-thumb { background: rgba(148,163,184,0.2); border-radius: 3px; }
.sglv-freq-summary-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 14px; }
.sglv-freq-summary-card { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 8px 6px; background: rgba(255,255,255,0.03); border: 1px solid rgba(148,163,184,0.1); border-radius: 8px; text-align: center; }
.sglv-freq-summary-label { font-size: 9px; color: #64748b; font-weight: 600; }
.sglv-freq-summary-value { font-size: 16px; font-weight: 700; color: #e2e8f0; }
.sglv-freq-summary-sub { font-size: 9px; color: #475569; }
.sglv-freq-chart-section { margin-bottom: 14px; }
.sglv-freq-chart-title { font-size: 11px; font-weight: 700; color: #94a3b8; margin-bottom: 6px; }
.sglv-freq-chart svg rect { transition: opacity 0.15s; }
.sglv-freq-chart svg rect[data-v] { cursor: pointer; }
.sglv-freq-chart-labels { display: flex; font-size: 9px; color: #475569; margin-top: 3px; }
.sglv-freq-chart-labels span { flex: 1; text-align: center; }
.sglv-freq-chart-labels-hr span { font-size: 8px; }
/* === v2.9.22: 游玩数据页——家庭成员对比紧凑到右半侧底部,Top 20 撑到更高视野 === */
.sglv-playtime-split-right {
gap: 0;
}
/* 家庭对比放右半侧后紧凑显示:限制高度 + 内部滚动 */
.sglv-playtime-split-right > .sglv-family-compare-section {
flex-shrink: 0;
margin: 10px 0 0;
padding: 10px 12px;
max-height: 200px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sglv-playtime-split-right > .sglv-family-compare-section .sglv-playtime-section-title {
margin-bottom: 6px;
font-size: 12px;
}
.sglv-playtime-split-right > .sglv-family-compare-section .sglv-family-compare-container {
margin-top: 0;
max-height: 140px;
overflow-y: auto;
}
/* v2.9.69: 游戏橱窗 KPI 扁平化——与统计仪表/入库趋势页一致,左侧色条+紧凑 padding */
.sglv-stats-dashboard .sglv-stat-dash-card { padding: 6px 10px 5px !important; border-left-width: 3px !important; }
@keyframes sglv-skel-pulse {
0%, 100% { opacity: 0.55; }
50% { opacity: 0.9; }
}
@keyframes sglv-skel-shimmer-x {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.sglv-heatmap-skeleton svg rect { animation: sglv-skel-pulse 1.6s ease-in-out infinite; }
.sglv-heatmap-skeleton svg rect:nth-child(odd) { animation-delay: 0.15s; }
.sglv-heatmap-skeleton svg rect:nth-child(3n) { animation-delay: 0.3s; }
.sglv-heatmap-mini-skeleton rect { animation: sglv-skel-pulse 1.4s ease-in-out infinite; }
.sglv-heatmap-mini-skeleton rect:nth-child(odd) { animation-delay: 0.2s; }
.sglv-family-bar-skel .sglv-family-bar-track { background: rgba(255, 255, 255, 0.04) !important; }
.sglv-family-bar-skel .sglv-family-bar-name,
.sglv-family-bar-skel .sglv-family-bar-hours {
background: linear-gradient(90deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.04) 100%) !important;
background-size: 200% 100%;
animation: sglv-skel-shimmer-x 1.6s linear infinite;
}
.sglv-family-bar-skel:nth-child(2) .sglv-family-bar-name,
.sglv-family-bar-skel:nth-child(2) .sglv-family-bar-hours { animation-delay: 0.15s; }
.sglv-family-bar-skel:nth-child(3) .sglv-family-bar-name,
.sglv-family-bar-skel:nth-child(3) .sglv-family-bar-hours { animation-delay: 0.3s; }
.sglv-family-bar-skel:nth-child(4) .sglv-family-bar-name,
.sglv-family-bar-skel:nth-child(4) .sglv-family-bar-hours { animation-delay: 0.45s; }`;
const CSS_SGIS_ENHANCE =` /* === tab 容器:增加底部高光 + tab 之间分隔感 === */
.sgis-tabs { position: relative; }
.sgis-tabs::after {
content: ""; position: absolute; left: 12px; right: 12px; bottom: -1px; height: 1px;
background: linear-gradient(90deg, transparent, rgba(139, 92, 246, 0.35), transparent);
pointer-events: none;
}
/* === tab 本身:图标上浮 + 文字微微缩放 === */
.sgis-tab .sgis-svg {
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.sgis-tab:hover .sgis-svg {
transform: translateY(-1px) scale(1.08);
}
.sgis-tab.active .sgis-svg {
transform: translateY(-0.5px) scale(1.12);
filter: drop-shadow(0 1px 4px rgba(139, 92, 246, 0.45));
}
.sgis-tab span {
transition: font-weight 0.2s, letter-spacing 0.2s;
}
.sgis-tab.active span {
font-weight: 700;
letter-spacing: 0.3px;
}
/* === 标题图标:更精致渐变 + 内阴影 === */
.sgis-title-icon {
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 45%, #3b82f6 100%) !important;
box-shadow:
0 4px 12px rgba(139, 92, 246, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.25),
inset 0 -1px 0 rgba(0, 0, 0, 0.2) !important;
}
.sgis-title-icon::after {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0%, transparent 45%) !important;
}
/* === 右上角 action 按钮:加 active 反馈 === */
.sgis-icon-btn:active {
transform: translateY(0) scale(0.94) !important;
}
.sgis-icon-btn.sgis-spin svg {
animation: sgis-spin 0.8s linear infinite, sgis-pulse 1.4s ease-in-out infinite;
}
@keyframes sgis-pulse {
0%, 100% { box-shadow: 0 2px 8px rgba(139, 92, 246, 0.25); }
50% { box-shadow: 0 2px 14px rgba(139, 92, 246, 0.6); }
}
/* === SGIS 整体滚动条更精致 === */
.sgis-body::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(139, 92, 246, 0.45), rgba(59, 130, 246, 0.45)) !important;
}
/* === SGIS 状态指示器(用于加载/错误/空状态) === */
.sgis-state-icon {
width: 48px; height: 48px; border-radius: 14px;
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(59, 130, 246, 0.08));
border: 1px solid rgba(139, 92, 246, 0.2);
display: inline-flex; align-items: center; justify-content: center;
margin-bottom: 10px; color: #a78bfa;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.sgis-state-icon svg { width: 22px; height: 22px; }
/* === 错误状态用红 === */
.sgis-state.error .sgis-state-icon {
background: linear-gradient(135deg, rgba(244, 63, 94, 0.15), rgba(244, 63, 94, 0.05));
border-color: rgba(244, 63, 94, 0.3);
color: #f87171;
}
/* === v2.9.15: 徽章标签页英雄区(顶部总览) === */
.sgis-badge-hero {
position: relative;
background: linear-gradient(135deg, rgba(139, 92, 246, 0.18) 0%, rgba(59, 130, 246, 0.1) 50%, var(--sgis-bg-2) 100%);
border: 1px solid rgba(139, 92, 246, 0.3);
border-radius: 12px;
padding: 14px 14px 12px;
margin-bottom: 10px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.sgis-badge-hero::before { content: ""; position: absolute; top: -40px; right: -30px; width: 140px; height: 140px; background: radial-gradient(circle, rgba(139, 92, 246, 0.25) 0%, transparent 65%); pointer-events: none; }
.sgis-badge-hero::after { content: ""; position: absolute; bottom: -30px; left: -20px; width: 100px; height: 100px; background: radial-gradient(circle, rgba(6, 182, 212, 0.15) 0%, transparent 60%); pointer-events: none; }
.sgis-badge-hero > * { position: relative; z-index: 1; }
.sgis-badge-hero-stats {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px;
margin-bottom: 10px;
}
.sgis-badge-hero-stat { text-align: center; padding: 6px 4px; background: rgba(255, 255, 255, 0.04); border-radius: 7px; border: 1px solid rgba(255, 255, 255, 0.05); }
.sgis-badge-hero-stat.highlight { background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(20, 184, 166, 0.08)); border-color: rgba(16, 185, 129, 0.3); }
.sgis-badge-hero-stat-val { font-size: 18px; font-weight: 800; color: #fff; line-height: 1.1; font-variant-numeric: tabular-nums; letter-spacing: -0.5px; }
.sgis-badge-hero-stat.highlight .sgis-badge-hero-stat-val { color: #34d399; }
.sgis-badge-hero-stat-lbl { font-size: 9px; color: var(--sgis-text-2); margin-top: 2px; font-weight: 600; }
.sgis-badge-hero-stat.highlight .sgis-badge-hero-stat-lbl { color: #6ee7b7; }
.sgis-badge-hero-progress { margin-bottom: 12px; }
.sgis-badge-hero-progress-label { display: flex; justify-content: space-between; font-size: 10px; color: var(--sgis-text-2); margin-bottom: 4px; font-weight: 600; }
.sgis-badge-hero-progress-label span:last-child { color: #c4b5fd; }
.sgis-badge-hero-progress-bar { height: 6px; background: rgba(255, 255, 255, 0.06); border-radius: 3px; overflow: hidden; box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3); }
.sgis-badge-hero-progress-fill { height: 100%; background: linear-gradient(90deg, #10b981 0%, #34d399 100%); border-radius: 3px; transition: width 0.5s; box-shadow: 0 0 6px rgba(16, 185, 129, 0.4); }
.sgis-badge-hero-levels { padding-top: 10px; border-top: 1px dashed rgba(255, 255, 255, 0.08); }
.sgis-badge-hero-levels-label { font-size: 10px; color: var(--sgis-text-2); margin-bottom: 6px; font-weight: 600; }
.sgis-badge-hero-levels-bars { display: flex; align-items: flex-end; justify-content: space-around; gap: 4px; height: 50px; }
.sgis-badge-hero-level-cell { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 0; }
.sgis-badge-hero-level-bar {
width: 100%; min-height: 3px; max-width: 24px;
background: linear-gradient(180deg, #8b5cf6 0%, #6366f1 100%);
border-radius: 3px 3px 0 0; flex: 1; align-self: flex-end;
display: flex; align-items: flex-start; justify-content: center; padding-top: 2px;
box-shadow: 0 0 6px rgba(139, 92, 246, 0.3);
transition: height 0.4s;
}
.sgis-badge-hero-level-count { font-size: 9px; color: #fff; font-weight: 700; font-variant-numeric: tabular-nums; }
.sgis-badge-hero-level-num { font-size: 9px; color: var(--sgis-text-2); font-weight: 600; }
/* === v2.9.15: 章节标题 meta(右侧次要信息) === */
.sgis-section-title-meta { font-size: 10px; color: var(--sgis-text-2); font-weight: 500; text-transform: none; letter-spacing: 0; margin-left: auto; }
/* === v2.9.38: 洞察页成就引导入口(替代旧折叠列表) === */
.sgis-insight-ach-entry {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: linear-gradient(135deg, rgba(251, 191, 36, 0.08), rgba(245, 158, 11, 0.04));
border: 1px solid rgba(251, 191, 36, 0.18);
border-radius: 10px;
transition: all 0.25s ease;
margin-top: 2px;
}
.sgis-insight-ach-entry:hover {
background: linear-gradient(135deg, rgba(251, 191, 36, 0.14), rgba(245, 158, 11, 0.08));
border-color: rgba(251, 191, 36, 0.35);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(251, 191, 36, 0.12);
}
.sgis-insight-ach-entry > svg {
width: 36px;
height: 36px;
flex-shrink: 0;
filter: drop-shadow(0 2px 4px rgba(251, 191, 36, 0.25));
}
.sgis-insight-ach-entry-info {
flex: 1;
min-width: 0;
}
.sgis-insight-ach-entry-title {
font-size: 12px;
font-weight: 700;
color: #fef3c7;
margin-bottom: 2px;
line-height: 1.3;
}
.sgis-insight-ach-entry-sub {
font-size: 10px;
color: var(--sgis-text-2);
line-height: 1.4;
}
.sgis-insight-ach-entry-btn {
flex-shrink: 0;
padding: 5px 10px;
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: #1c0a01;
border: none;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.sgis-insight-ach-entry-btn:hover {
background: linear-gradient(135deg, #fcd34d, #fbbf24);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(251, 191, 36, 0.4);
}
.sgis-insight-ach-entry-btn:active {
transform: translateY(0);
}
/* === v2.9.15: 徽章 group 头增强(加进度条 + 已合成数) === */
.sgis-badge-group-head { gap: 10px; padding: 9px 12px; }
.sgis-badge-group-head:hover { background: linear-gradient(135deg, rgba(139, 92, 246, 0.05), rgba(59, 130, 246, 0.02)); }
.sgis-badge-group-progress { width: 100%; margin-top: 6px; height: 3px; background: rgba(255, 255, 255, 0.05); border-radius: 2px; overflow: hidden; }
.sgis-badge-group-progress-fill { height: 100%; background: linear-gradient(90deg, #10b981, #34d399); border-radius: 2px; }
.sgis-badge-group-stats { flex-direction: column; align-items: flex-end; gap: 3px; }
.sgis-badge-group-stat-val { font-size: 11px; font-weight: 700; color: #c4b5fd; }
.sgis-badge-group-stat-lbl { font-size: 9px; color: var(--sgis-text-3); margin-left: 4px; }
/* === v2.9.15: 徽章 footer 折叠说明 === */
.sgis-badge-footer-details {
margin-top: 14px; padding: 8px 10px;
background: rgba(255, 255, 255, 0.02); border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 7px; font-size: 10px; color: var(--sgis-text-2);
}
.sgis-badge-footer-details summary {
cursor: pointer; font-weight: 600; user-select: none;
list-style: none; display: flex; align-items: center; gap: 5px;
}
.sgis-badge-footer-details summary::-webkit-details-marker { display: none; }
.sgis-badge-footer-details summary::before { content: "▸"; display: inline-block; transition: transform 0.2s; font-size: 9px; color: var(--sgis-text-3); }
.sgis-badge-footer-details[open] summary::before { transform: rotate(90deg); }
.sgis-badge-footer-content { padding-top: 8px; line-height: 1.6; }
.sgis-badge-footer-formula { margin-bottom: 4px; }
.sgis-badge-footer-formula b { color: #c4b5fd; }
.sgis-badge-footer-breakdown { font-size: 9px; color: var(--sgis-text-3); }
.sgis-badge-footer-meta { font-size: 9px; color: var(--sgis-text-3); margin-top: 2px; }
/* === v2.9.15 → v2.9.18 → v2.9.21: 价值卡片单列布局(共享库默认 flex 单列,在窄侧边栏里 2 列挤压变形) ===
v2.9.18 尝试 2 列 grid + minmax(0,1fr) + 三层 min-width:0 链式压制,但实际渲染出单卡仍被竖向挤压
(游戏名截断为 S..、Lv./50/XP 竖排)—— 在 300px 宽的 SGIS 侧边栏里 2 列宽度仍不够放下 56px img + 38px score + 名字 + market。
v2.9.21 退回单列布局,删掉所有过紧的子项压缩样式,卡片按 flex 自然撑开,信息完整可读。
历史: 共享库 sglv-shared-css.lib.js:1467 的 .sgis-value-cards { display:flex; flex-direction:column; gap:6px; } 直接可用。
这里只追加 !important 防止共享库默认被覆盖即可,不再做 2 列。
*/
.sgis-value-cards {
display: flex !important;
flex-direction: column !important;
gap: 6px !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
/* 单元本身保持原样,不压制 min-width,让 flex 子项(img 56 + score 38 + market 文本)自然撑开 */
/* === v2.9.17: 愿望单首次加载骨架屏(避免页面紧缩) === */
@keyframes sglv-skel-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
.sglv-skel-bar {
background: linear-gradient(90deg, rgba(255, 255, 255, 0.04) 0%, rgba(139, 92, 246, 0.12) 50%, rgba(255, 255, 255, 0.04) 100%);
background-size: 200% 100%;
animation: sglv-skel-shimmer 1.6s linear infinite;
border-radius: 4px;
}
.sglv-wl-kpi {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
margin-bottom: 10px;
}
.sglv-wl-kpi-cell {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.06), rgba(59, 130, 246, 0.03));
border: 1px solid rgba(139, 92, 246, 0.15);
border-radius: 8px;
padding: 10px 8px;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.sglv-skel-num { width: 50%; height: 18px; }
.sglv-skel-lbl { width: 70%; height: 9px; }
.sglv-skel-section-title { width: 40%; height: 12px; margin-bottom: 8px; }
.sglv-skel-row { width: 100%; height: 10px; margin-bottom: 6px; opacity: 0.85; }
.sglv-skel-row-short { width: 60%; opacity: 0.6; }
.sglv-skel-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
padding: 4px 0;
}
.sglv-skel-game-card {
background: rgba(255, 255, 255, 0.025);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.sglv-skel-cover {
width: 100%;
aspect-ratio: 184 / 69;
background: linear-gradient(90deg, rgba(139, 92, 246, 0.08) 0%, rgba(139, 92, 246, 0.18) 50%, rgba(139, 92, 246, 0.08) 100%);
background-size: 200% 100%;
animation: sglv-skel-shimmer 1.6s linear infinite;
border-radius: 4px;
}
.sglv-skel-card-name { width: 80%; height: 11px; }
.sglv-skel-card-meta { width: 50%; height: 9px; }
.sglv-wl-skel-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 18px 12px;
margin-top: 12px;
color: var(--sglv-text-secondary, #94a3b8);
font-size: 12px;
font-weight: 500;
}
.sglv-wl-skel-tip .sglv-spinner { width: 16px; height: 16px; border-width: 2px; }
/* === v2.9.47: 愿望单加载等待动画(参考 DLC spinner 风格) === */
.sglv-wl-loading { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:48px 20px; gap:14px; color:#94a3b8; min-height:280px; }
.sglv-wl-loading .sglv-wl-spin { width:32px; height:32px; animation:sglv-spin 1s linear infinite; color:#a78bfa; }
.sglv-wl-loading-text { font-size:13px; font-weight:600; color:#cbd5e1; }
.sglv-wl-loading-sub { font-size:11px; color:#64748b; }
.sglv-wl-loading-bar { width:220px; height:3px; background:rgba(102,192,244,0.12); border-radius:2px; overflow:hidden; }
.sglv-wl-loading-bar-fill { height:100%; background:linear-gradient(90deg,#66c0f4,#a78bfa); border-radius:2px; transition:width 0.4s ease; }`;
const CSS_TOAST =` @keyframes sglv-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.sglv-toast { position: fixed; bottom: 50px; left: 50%; transform: translateX(-50%) translateY(20px);
background: rgba(15,23,42,0.95); color: #fff; border: 1px solid rgba(167,139,250,0.5);
padding: 10px 24px; border-radius: 20px; font-size: 13px; font-weight: 500;
box-shadow: 0 4px 16px rgba(0,0,0,0.3); z-index: 999999; opacity: 0; pointer-events: none;
transition: opacity 0.3s, transform 0.3s; }
.sglv-toast.sglv-toast-show { opacity: 1; transform: translateX(-50%) translateY(0); }
.sglv-toast.sglv-toast-success { border-color: #2ed573; }
.sglv-toast.sglv-toast-error { border-color: #ff6b6b; }
.sglv-toast.sglv-toast-warning { border-color: #f59e0b; }
.sglv-toast.sglv-toast-info { border-color: #54a0ff; }`;
const CSS_APIKEY_GUIDE =` @keyframes sglv-guide-in { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: translateX(0); } }
.sglv-guide-toast { position: fixed; right: 24px; bottom: 24px; z-index: 999999; width: 320px; box-sizing: border-box;
background: linear-gradient(145deg, rgba(30,41,59,0.98), rgba(15,23,42,0.98));
border: 1px solid rgba(167,139,250,0.45); border-radius: 14px; padding: 16px;
box-shadow: 0 12px 32px rgba(0,0,0,0.45); color: #e2e8f0; font-size: 13px;
animation: sglv-guide-in 0.3s ease-out; }
.sglv-guide-toast-head { display: flex; align-items: center; gap: 8px; }
.sglv-guide-toast-title { flex: 1; display: inline-flex; align-items: center; gap: 6px; font-size: 14px; font-weight: 700; color: #fff; }
.sglv-guide-toast-title svg { width: 15px; height: 15px; color: #a78bfa; }
.sglv-guide-toast-close { background: none; border: none; color: #94a3b8; cursor: pointer; padding: 3px; line-height: 0; border-radius: 6px; }
.sglv-guide-toast-close:hover { color: #fff; background: rgba(148,163,184,0.15); }
.sglv-guide-toast-close svg { width: 13px; height: 13px; }
.sglv-guide-toast-desc { margin: 8px 0 12px; color: #94a3b8; font-size: 12px; line-height: 1.7; }
.sglv-guide-toast-actions { display: flex; gap: 8px; }
.sglv-guide-toast-btn { flex: 1; padding: 8px 0; border-radius: 8px; font-size: 12px; font-weight: 600; cursor: pointer; border: 1px solid transparent; transition: filter 0.15s; }
.sglv-guide-toast-btn.primary { background: linear-gradient(90deg, #8b5cf6, #3b82f6); color: #fff; }
.sglv-guide-toast-btn.ghost { background: transparent; border-color: rgba(148,163,184,0.3); color: #cbd5e1; }
.sglv-guide-toast-btn:hover { filter: brightness(1.15); }`;
const CSS_SETTINGS =` .sglv-panel .sglv-settings-btn.sglv-active { background: rgba(102,192,244,0.2) !important; color: #66c0f4 !important; box-shadow: inset 0 0 0 1px rgba(102,192,244,0.5); border-radius: 6px; }
.sglv-set-wrap { display: flex; flex-direction: column; height: 100%; min-height: 0; }
.sglv-set-topbar { display: flex; align-items: center; gap: 12px; padding: 14px 18px 10px; flex: none; border-bottom: 1px solid rgba(148,163,184,0.12); }
.sglv-set-back { display: inline-flex; align-items: center; gap: 6px; background: rgba(148,163,184,0.1); border: 1px solid rgba(148,163,184,0.2); color: #cbd5e1; font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: 8px; cursor: pointer; transition: background 0.15s, color 0.15s; }
.sglv-set-back:hover { background: rgba(148,163,184,0.22); color: #fff; }
.sglv-set-title { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700; color: #fff; }
.sglv-set-title svg { width: 16px; height: 16px; color: #66c0f4; }
.sglv-set-grid { flex: 1; min-height: 0; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 14px; padding: 16px 18px; align-content: start; }
.sglv-set-card { background: rgba(15,23,42,0.55); border: 1px solid rgba(148,163,184,0.14); border-radius: 12px; padding: 16px; transition: border-color 0.15s; }
.sglv-set-card:hover { border-color: rgba(102,192,244,0.35); }
.sglv-set-card.wide { grid-column: 1 / -1; }
.sglv-set-card-title { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 700; color: #e2e8f0; margin-bottom: 8px; }
.sglv-set-chip { display: inline-flex; width: 26px; height: 26px; align-items: center; justify-content: center; border-radius: 8px; flex: none; }
.sglv-set-chip svg { width: 14px; height: 14px; }
.sglv-set-chip.blue { background: rgba(102,192,244,0.15); color: #66c0f4; }
.sglv-set-chip.purple { background: rgba(167,139,250,0.15); color: #a78bfa; }
.sglv-set-chip.amber { background: rgba(245,158,11,0.15); color: #f59e0b; }
.sglv-set-desc { font-size: 12px; color: #94a3b8; line-height: 1.7; margin-bottom: 12px; }
.sglv-set-desc a { color: #66c0f4; text-decoration: none; }
.sglv-set-desc a:hover { text-decoration: underline; }
.sglv-set-field { margin-bottom: 12px; }
.sglv-set-field:last-child { margin-bottom: 0; }
.sglv-set-field > label { display: block; font-size: 12px; font-weight: 600; color: #cbd5e1; margin-bottom: 6px; }
.sglv-set-input { width: 100%; box-sizing: border-box; background: rgba(2,6,23,0.6); border: 1px solid rgba(148,163,184,0.22); border-radius: 8px; padding: 9px 12px; color: #e2e8f0; font-size: 13px; transition: border-color 0.15s, box-shadow 0.15s; }
.sglv-set-input::placeholder { color: #475569; }
.sglv-set-input:focus { outline: none; border-color: #66c0f4; box-shadow: 0 0 0 3px rgba(102,192,244,0.15); }
.sglv-set-hint { font-size: 11px; color: #64748b; margin-top: 4px; line-height: 1.5; }
.sglv-set-mode-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 10px; }
.sglv-set-mode-item { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; border: 1px solid rgba(148,163,184,0.16); border-radius: 10px; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
.sglv-set-mode-item:hover { border-color: rgba(167,139,250,0.4); background: rgba(167,139,250,0.06); }
.sglv-set-mode-item input { margin-top: 3px; accent-color: #a78bfa; cursor: pointer; }
.sglv-set-mode-item input:checked ~ .sglv-set-mode-info .sglv-set-mode-name { color: #a78bfa; }
.sglv-set-mode-name { font-size: 12.5px; font-weight: 700; color: #e2e8f0; }
.sglv-set-mode-desc { font-size: 11.5px; color: #94a3b8; margin-top: 2px; line-height: 1.5; }
/* v2.9.46: 设置页面开关样式 */
.sglv-set-chip.green { background: rgba(75,181,79,0.15); color: #4bb54f; }
.sglv-set-toggle-row { display: flex !important; align-items: center; justify-content: space-between; font-size: 12.5px; font-weight: 600; color: #cbd5e1; cursor: pointer; user-select: none; }
.sglv-set-switch { position: relative; display: inline-block; width: 36px; height: 20px; flex: none; }
.sglv-set-switch input { opacity: 0; width: 0; height: 0; }
.sglv-set-slider { position: absolute; cursor: pointer; inset: 0; background-color: rgba(255,255,255,0.1); transition: .2s; border-radius: 20px; border: 1px solid rgba(255,255,255,0.05); }
.sglv-set-slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 2px; bottom: 2px; background-color: #8a9ba8; transition: .2s; border-radius: 50%; }
.sglv-set-switch input:checked + .sglv-set-slider { background-color: rgba(75,181,79,0.2); border-color: rgba(75,181,79,0.4); }
.sglv-set-switch input:checked + .sglv-set-slider:before { transform: translateX(16px); background-color: #4bb54f; }
/* v2.11.40: 外观主题选择卡(sglv-set-theme-swatch 为主题预览色板,颜色刻意固定不随主题切换——生成器跳过) */
.sglv-set-theme-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.sglv-set-theme-opt { position: relative; display: flex; flex-direction: column; gap: 8px; padding: 10px; border: 1px solid rgba(148,163,184,0.16); border-radius: 10px; cursor: pointer; transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; }
.sglv-set-theme-opt:hover { border-color: rgba(167,139,250,0.4); }
.sglv-set-theme-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
.sglv-set-theme-swatch { height: 34px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.08); }
.sglv-set-theme-swatch.purple { background: linear-gradient(135deg, #171a21 0%, #4c1d95 55%, #a78bfa 100%); }
.sglv-set-theme-swatch.blue { background: linear-gradient(135deg, #171a21 0%, #2a475e 55%, #66c0f4 100%); }
.sglv-set-theme-name { display: flex; align-items: center; justify-content: space-between; font-size: 12.5px; font-weight: 700; color: #e2e8f0; }
.sglv-set-theme-name .sglv-set-theme-check { color: #4bb54f; font-size: 12px; opacity: 0; }
.sglv-set-theme-opt.selected { border-color: rgba(167,139,250,0.55); background: rgba(167,139,250,0.07); box-shadow: 0 0 0 1px rgba(167,139,250,0.25); }
.sglv-set-theme-opt.selected .sglv-set-theme-check { opacity: 1; }
.sglv-set-theme-desc { font-size: 11px; color: #94a3b8; line-height: 1.5; }
.sglv-set-savebar { display: flex; align-items: center; gap: 12px; padding: 12px 18px; flex: none; border-top: 1px solid rgba(148,163,184,0.12); background: rgba(2,6,23,0.35); }
.sglv-set-save-btn { display: inline-flex; align-items: center; gap: 6px; background: linear-gradient(90deg, #8b5cf6, #3b82f6); color: #fff; border: none; border-radius: 8px; padding: 9px 22px; font-size: 13px; font-weight: 700; cursor: pointer; transition: filter 0.15s, transform 0.1s; }
.sglv-set-save-btn:hover { filter: brightness(1.15); }
.sglv-set-save-btn:active { transform: scale(0.97); }
.sglv-set-status { font-size: 12px; color: #2ed573; font-weight: 600; }`;
const CSS_KPI =` .sglv-stat-dash-card.orange { border-color: rgba(245, 158, 11, 0.3); background: rgba(245, 158, 11, 0.08); }
.sglv-stat-dash-card.orange .sglv-stat-dash-icon { color: #f59e0b; }
.sglv-stat-dash-card.orange .sglv-stat-dash-value { color: #f59e0b; }
.sglv-stat-dash-card.teal { border-color: rgba(20, 184, 166, 0.3); background: rgba(20, 184, 166, 0.08); }
.sglv-stat-dash-card.teal .sglv-stat-dash-icon { color: #14b8a6; }
.sglv-stat-dash-card.teal .sglv-stat-dash-value { color: #14b8a6; }
.sglv-stat-dash-card.pink { border-color: rgba(236, 72, 153, 0.3); background: rgba(236, 72, 153, 0.08); }
.sglv-stat-dash-card.pink .sglv-stat-dash-icon { color: #ec4899; }
.sglv-stat-dash-card.pink .sglv-stat-dash-value { color: #ec4899; }
.sglv-bundle-badge { display: inline-block; margin-left: 4px; padding: 1px 5px; font-size: 10px; font-weight: 600; border-radius: 3px; background: #f59e0b; color: #fff; vertical-align: middle; cursor: help; }
/* v2.9.69: 游戏橱窗 KPI 改为 grid 7 列扁平布局,与统计仪表/趋势页一致 */
.sglv-stats-dashboard { display: grid !important; grid-template-columns: repeat(7, 1fr) !important; gap: 6px !important; }
@media (max-width: 1280px) { .sglv-stats-dashboard { grid-template-columns: repeat(4, 1fr) !important; } }
@media (max-width: 768px) { .sglv-stats-dashboard { grid-template-columns: repeat(3, 1fr) !important; } }
@media (max-width: 480px) { .sglv-stats-dashboard { grid-template-columns: repeat(2, 1fr) !important; } }
.sglv-stats-dashboard .sglv-stat-dash-card { min-width: 0; cursor: pointer; transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s; }
.sglv-stats-dashboard .sglv-stat-dash-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.sglv-stats-dashboard .sglv-stat-dash-card.active { box-shadow: 0 0 0 2px rgba(102,192,244,0.4); border-left-width: 4px !important; }
.sglv-stats-dashboard .sglv-stat-dash-card.amber { cursor: default; }
.sglv-stats-dashboard .sglv-stat-dash-card.amber:hover { transform: none; box-shadow: none; }`;
const CSS_SUBSTATUS =` .sgis-sub-status { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.sgis-sub-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; font-size: 11px; font-weight: 600; border-radius: 4px; }
/* v2.9.13: 年度大作 KPI 卡片可点击筛选样式 */
.sglv-trend-kpi-card.clickable { cursor: pointer; transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s; }
.sglv-trend-kpi-card.clickable:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.sglv-trend-kpi-card.active { border-width: 2px; box-shadow: 0 0 0 2px rgba(102,192,244,0.4); }`;
const CSS_COVER =` /* ====== 横版封面视图(3列,467/181 胶囊横幅) ====== */
.sglv-cover-grid { display: grid !important; grid-template-columns: repeat(3, 1fr) !important; gap: 12px !important; }
@media (max-width: 1100px) { .sglv-cover-grid { grid-template-columns: repeat(2, 1fr) !important; } }
@media (max-width: 700px) { .sglv-cover-grid { grid-template-columns: 1fr !important; } }
.sglv-cover-card {
background: linear-gradient(135deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.02) 100%) !important;
border: 1px solid rgba(102,192,244,0.1) !important;
border-radius: 10px !important;
overflow: hidden !important;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s !important;
cursor: pointer !important;
display: flex !important;
flex-direction: column !important;
}
.sglv-cover-card:hover {
border-color: rgba(102,192,244,0.35) !important;
transform: translateY(-3px) !important;
box-shadow: 0 6px 20px rgba(102,192,244,0.12), 0 2px 8px rgba(0,0,0,0.3) !important;
}
.sglv-cover-img-wrap {
position: relative !important;
width: 100% !important;
aspect-ratio: 467/181 !important;
overflow: hidden !important;
background: rgba(255,255,255,0.05) !important;
flex-shrink: 0 !important;
}
.sglv-cover-img { width: 100% !important; height: 100% !important; object-fit: cover !important; display: block !important; transition: transform 0.3s !important; }
.sglv-cover-card:hover .sglv-cover-img { transform: scale(1.04) !important; }
.sglv-cover-gradient {
position: absolute !important; bottom: 0 !important; left: 0 !important; right: 0 !important;
height: 45% !important;
background: linear-gradient(to top, rgba(13,18,35,0.85) 0%, transparent 100%) !important;
pointer-events: none !important; z-index: 1 !important;
}
.sglv-cover-badges { position: absolute !important; top: 6px !important; left: 6px !important; display: flex !important; gap: 4px !important; z-index: 2 !important; }
.sglv-cover-badge {
display: inline-flex !important; align-items: center !important; gap: 3px !important;
font-size: 10px !important; font-weight: 700 !important; padding: 2px 7px !important;
border-radius: 4px !important; backdrop-filter: blur(6px) !important; -webkit-backdrop-filter: blur(6px) !important;
background: rgba(0,0,0,0.55) !important; color: #c4b5fd !important; line-height: 1.3 !important;
}
.sglv-cover-badge svg { width: 10px !important; height: 10px !important; opacity: 0.85 !important; }
.sglv-cover-badge.shared { background: rgba(139,92,246,0.55) !important; color: #ddd6fe !important; }
.sglv-cover-badge.bundle { background: rgba(255,152,0,0.55) !important; color: #ffd591 !important; }
.sglv-cover-playtime {
position: absolute !important; bottom: 6px !important; right: 6px !important;
display: inline-flex !important; align-items: center !important; gap: 3px !important;
font-size: 11px !important; font-weight: 700 !important; padding: 2px 8px !important;
border-radius: 4px !important; background: rgba(0,0,0,0.65) !important; color: #66c0f4 !important;
backdrop-filter: blur(6px) !important; -webkit-backdrop-filter: blur(6px) !important;
z-index: 2 !important; line-height: 1.3 !important;
}
.sglv-cover-playtime svg { width: 10px !important; height: 10px !important; opacity: 0.85 !important; }
.sglv-cover-body { padding: 8px 10px 9px !important; display: flex !important; flex-direction: column !important; gap: 3px !important; }
.sglv-cover-name { font-size: 13px !important; font-weight: 600 !important; color: #e0e0e0 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; }
.sglv-cover-meta { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.sglv-cover-id { font-size: 10px !important; color: var(--sglv-text-secondary) !important; font-variant-numeric: tabular-nums !important; }
/* ====== 卡片视图增强 ====== */
.sglv-game-card { border-radius: 10px !important; }
.sglv-game-card:hover { box-shadow: 0 6px 20px rgba(102,192,244,0.1), 0 2px 8px rgba(0,0,0,0.3) !important; }
.sglv-card-img { transition: transform 0.3s !important; }
.sglv-game-card:hover .sglv-card-img { transform: scale(1.05) !important; }
/* ====== 工具栏排版优化 ====== */
.sglv-toolbar { gap: 8px !important; }`;
const CSS_PRICE =` /* === 9区完整价格对比表 === */
.sgis-price-tbl { display:flex; flex-direction:column; gap:1px; }
.sgis-price-tbl-header {
display:grid; grid-template-columns: 1.6fr 0.9fr 1fr 0.6fr 0.9fr;
gap:2px; padding:4px 6px; font-size:9px; color:var(--sgis-text-2);
text-transform:uppercase; letter-spacing:0.3px; border-bottom:1px solid rgba(255,255,255,0.06);
}
.sgis-price-tbl-row {
display:grid; grid-template-columns: 1.6fr 0.9fr 1fr 0.6fr 0.9fr;
gap:2px; padding:5px 6px; font-size:11px; align-items:center;
border-radius:4px; transition:background 0.15s;
}
.sgis-price-tbl-row:hover { background:rgba(255,255,255,0.04); }
.sgis-price-tbl-row-lowest { background:rgba(75,181,79,0.12) !important; border-left:3px solid #4bb54f; }
.sgis-price-tbl-row-highest { background:rgba(230,57,70,0.10) !important; border-left:3px solid #e63946; }
.sgis-price-tbl-row.user-region { box-shadow: inset 0 0 0 1px rgba(167,139,250,0.3); }
.sgis-price-tbl-cell { display:flex; align-items:center; gap:3px; overflow:hidden; }
.sgis-price-tbl-region { gap:4px; }
.sgis-price-tbl-initial { color:var(--sgis-text-2); font-size:10px; }
.sgis-price-tbl-price { font-weight:600; }
.sgis-price-tbl-cny { font-weight:700; color:#66c0f4; }
.sgis-price-tbl-badge-low { font-size:8px; background:#4bb54f; color:#fff; padding:1px 4px; border-radius:3px; margin-left:2px; }
.sgis-price-tbl-badge-high { font-size:8px; background:#e63946; color:#fff; padding:1px 4px; border-radius:3px; margin-left:2px; }
/* === v2.11.47: 国旗完整显示 — flagcdn w40 各国旗帜宽高比不一(US/GB/KZ/CO 为 2:1 宽旗,
BR 为 10:7 等),共享库 .sgis-price-flag 固定 30×20(3:2)+cover 会把宽旗左右裁掉约 1/4;
此处以更高优先级覆盖为 contain,旗帜图案完整呈现,比例空隙由行背景自然过渡 === */
.sgis-price-tbl .sgis-price-flag { object-fit: contain; }
.sgis-price-featured-img { object-fit: contain; }
/* === v2.11.47: 比价目标货币切换(多地区低价排行页头) === */
.sgis-price-cur-switch { margin-left:auto; display:inline-flex; align-items:center; gap:4px; flex-shrink:0; text-transform:none; letter-spacing:0; }
.sgis-price-cur-switch-label { font-size:9px; color:var(--sgis-text-2); font-weight:500; }
.sgis-price-cur-select {
font-size:10px; font-weight:600; color:#66c0f4; font-family:inherit;
background:rgba(102,192,244,0.08); border:1px solid rgba(102,192,244,0.25);
border-radius:6px; padding:2px 4px; cursor:pointer; outline:none;
max-width:132px; text-transform:none; letter-spacing:0; transition:border-color 0.15s, background 0.15s;
}
.sgis-price-cur-select:hover { border-color:rgba(102,192,244,0.5); background:rgba(102,192,244,0.14); }
.sgis-price-cur-select:focus { border-color:rgba(102,192,244,0.6); }
.sgis-price-cur-select option { background:#1b2838; color:#fff; }
.sgis-price-cur-fallback { font-size:9px; color:#c0a050; margin:-4px 0 6px; line-height:1.4; }
/* === 评测标签页 === */
/* === v2.9.15: 评测摘要(Steam 风格大字 + 渐变进度条) === */
.sgis-review-summary { display:flex; flex-direction:column; gap:10px; padding:10px 12px; background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.05); border-radius:8px; }
.sgis-review-summary-head { display:flex; align-items:center; gap:12px; }
.sgis-review-score { display:flex; align-items:center; gap:12px; flex:1; min-width:0; }
.sgis-review-score-icon { font-size:30px; line-height:1; flex-shrink:0; filter:drop-shadow(0 1px 2px rgba(0,0,0,0.3)); }
.sgis-review-score-meta { display:flex; flex-direction:column; gap:2px; min-width:0; flex:1; }
.sgis-review-score-text { font-size:14px; font-weight:700; line-height:1.2; }
.sgis-review-score-desc { font-size:10px; color:var(--sgis-text-2); line-height:1.3; }
.sgis-review-pct-big { font-size:24px; font-weight:800; font-variant-numeric:tabular-nums; line-height:1; flex-shrink:0; letter-spacing:-0.5px; }
.sgis-review-pct-big.pos { color:#4bb54f; }
.sgis-review-pct-big.mix { color:#a78bfa; }
.sgis-review-pct-big.neg { color:#e63946; }
.sgis-review-bar { display:flex; height:8px; border-radius:4px; overflow:hidden; background:rgba(230,57,70,0.3); box-shadow:inset 0 1px 2px rgba(0,0,0,0.3); }
.sgis-review-bar-positive { background:linear-gradient(90deg,#4bb54f 0%,#66c0f4 100%); height:100%; transition:width 0.5s cubic-bezier(0.4,0,0.2,1); box-shadow:0 0 6px rgba(75,181,79,0.4); }
.sgis-review-bar-positive.mix { background:linear-gradient(90deg,#a78bfa 0%,#54a0ff 100%); box-shadow:0 0 6px rgba(167,139,250,0.4); }
.sgis-review-bar-positive.neg { background:linear-gradient(90deg,#e63946 0%,#ff6b6b 100%); box-shadow:0 0 6px rgba(230,57,70,0.4); }
.sgis-review-counts { display:flex; justify-content:space-between; font-size:10px; font-weight:600; }
.sgis-review-counts .pos { color:#4bb54f; }
.sgis-review-counts .neg { color:#e63946; }
.sgis-review-trend { display:flex; gap:10px; font-size:10px; flex-wrap:wrap; padding:6px 8px; background:rgba(255,255,255,0.02); border-radius:6px; }
.sgis-review-trend-item { display:flex; flex-direction:column; gap:2px; flex:1; min-width:80px; }
.sgis-review-trend-label { color:var(--sgis-text-2); font-size:9px; }
.sgis-review-trend-val { font-weight:600; font-variant-numeric:tabular-nums; }
.sgis-review-filters { display:flex; flex-wrap:wrap; gap:4px; padding:6px 0; border-top:1px solid rgba(255,255,255,0.06); border-bottom:1px solid rgba(255,255,255,0.06); }
.sgis-review-filter-group { display:flex; align-items:center; gap:2px; }
.sgis-review-filter-label { font-size:9px; color:var(--sgis-text-2); margin-right:2px; }
.sgis-review-filter-btn {
font-size:10px; padding:2px 7px; border-radius:10px; border:1px solid rgba(255,255,255,0.1);
background:transparent; color:var(--sgis-text-2); cursor:pointer; transition:all 0.15s;
}
.sgis-review-filter-btn:hover { background:rgba(255,255,255,0.06); }
.sgis-review-filter-btn.active { background:rgba(102,192,244,0.2); color:#66c0f4; border-color:rgba(102,192,244,0.4); }
.sgis-review-list { display:flex; flex-direction:column; gap:8px; padding:4px 0; }
/* v2.9.15: 评测项卡片化——左右色条标识好评/差评 */
.sgis-review-item {
position:relative; padding:10px 10px 8px 12px; border-radius:8px;
background:rgba(255,255,255,0.025); border:1px solid rgba(255,255,255,0.05);
transition:all 0.2s ease; overflow:hidden;
}
.sgis-review-item::before {
content:""; position:absolute; left:0; top:0; bottom:0; width:3px;
background:linear-gradient(180deg,transparent,rgba(75,181,79,0.5),transparent);
opacity:0; transition:opacity 0.2s;
}
.sgis-review-item.is-positive::before { background:linear-gradient(180deg,transparent,#4bb54f,transparent); opacity:0.7; }
.sgis-review-item.is-negative::before { background:linear-gradient(180deg,transparent,#e63946,transparent); opacity:0.7; }
.sgis-review-item:hover { background:rgba(255,255,255,0.045); border-color:rgba(139,92,246,0.15); transform:translateY(-1px); box-shadow:0 2px 8px rgba(0,0,0,0.15); }
.sgis-review-item.is-positive:hover { border-color:rgba(75,181,79,0.25); }
.sgis-review-item.is-negative:hover { border-color:rgba(230,57,70,0.25); }
.sgis-review-item-header { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
.sgis-review-avatar { width:32px; height:32px; min-width:32px; border-radius:50%; background:rgba(255,255,255,0.08); display:flex; align-items:center; justify-content:center; object-fit:cover; flex-shrink:0; border:1px solid rgba(255,255,255,0.06); }
.sgis-review-avatar-link { display:inline-flex; line-height:0; flex-shrink:0; border-radius:50%; transition:transform 0.2s; }
.sgis-review-avatar-link:hover { transform:scale(1.08); }
.sgis-review-avatar-fallback {
background:linear-gradient(135deg,rgba(139,92,246,0.4),rgba(59,130,246,0.3));
color:#fff; font-weight:700; font-size:13px; letter-spacing:0.5px;
text-transform:uppercase;
}
.sgis-review-author-wrap { display:flex; flex-direction:column; gap:1px; min-width:0; flex:1; }
.sgis-review-author { font-size:12px; font-weight:600; color:#66c0f4; text-decoration:none; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; transition:color 0.15s; }
a.sgis-review-author:hover { color:#93c5fd; }
.sgis-review-date { font-size:10px; color:var(--sgis-text-3); font-weight:400; }
.sgis-review-rec { font-size:10px; padding:2px 7px; border-radius:4px; font-weight:700; flex-shrink:0; }
.sgis-review-rec-yes { background:rgba(75,181,79,0.18); color:#4bb54f; border:1px solid rgba(75,181,79,0.3); }
.sgis-review-rec-no { background:rgba(230,57,70,0.18); color:#f87171; border:1px solid rgba(230,57,70,0.3); }
.sgis-review-meta { font-size:10px; color:var(--sgis-text-2); display:flex; gap:8px; flex-wrap:wrap; margin:6px 0 4px; }
.sgis-review-meta-item { padding:1px 6px; background:rgba(255,255,255,0.04); border-radius:3px; }
/* v2.9.15: 计数摘要条——图标 + 数字 + 已筛选徽章 */
.sgis-review-count-summary {
display:flex; align-items:center; gap:6px; margin-top:8px;
padding:5px 8px; font-size:10px; color:var(--sgis-text-2);
background:rgba(139,92,246,0.06); border:1px solid rgba(139,92,246,0.12);
border-radius:5px;
}
.sgis-review-count-summary b { color:#c4b5fd; font-weight:700; }
.sgis-review-count-icon { width:12px; height:12px; color:#a78bfa; flex-shrink:0; }
.sgis-review-count-icon svg { width:12px; height:12px; }
.sgis-review-count-filtered {
margin-left:auto; font-size:9px; font-weight:700; padding:1px 6px;
background:rgba(167,139,250,0.2); color:#a78bfa;
border:1px solid rgba(167,139,250,0.3); border-radius:8px;
}
.sgis-review-text {
font-size:12px; line-height:1.6; color:var(--sgis-text); margin-top:4px;
display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
cursor:pointer; transition:all 0.15s; padding:4px 0; word-break:break-word;
}
.sgis-review-text:hover { color:#f8fafc; }
.sgis-review-text.expanded { -webkit-line-clamp:unset; overflow:visible; }
/* v2.9.15: 评测项操作行(展开/复制)——紧凑胶囊按钮 */
.sgis-review-actions { display:flex; align-items:center; gap:6px; margin-top:6px; }
.sgis-review-action-btn {
display:inline-flex; align-items:center; gap:4px;
padding:2px 8px; font-size:10px; font-weight:600;
background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08);
color:var(--sgis-text-2); border-radius:10px;
cursor:pointer; transition:all 0.15s;
}
.sgis-review-action-btn:hover { background:rgba(139,92,246,0.15); border-color:rgba(139,92,246,0.3); color:#c4b5fd; }
.sgis-review-action-btn svg { width:11px; height:11px; transition:transform 0.25s; }
.sgis-review-expand-btn.is-expanded svg { transform:rotate(180deg); }
.sgis-review-action-btn.copied { background:rgba(75,181,79,0.2); border-color:rgba(75,181,79,0.4); color:#4bb54f; }
.sgis-review-votes { font-size:10px; color:var(--sgis-text-3); margin-top:6px; display:flex; gap:10px; padding-top:6px; border-top:1px dashed rgba(255,255,255,0.06); }
/* v2.9.15: 评测空状态——精致状态点 + 文字 */
.sgis-review-empty {
text-align:center; padding:30px 16px; font-size:12px; color:var(--sgis-text-2);
display:flex; flex-direction:column; align-items:center; gap:6px;
}
.sgis-review-empty-icon {
width:42px; height:42px; border-radius:12px;
background:linear-gradient(135deg,rgba(139,92,246,0.12),rgba(59,130,246,0.06));
border:1px solid rgba(139,92,246,0.18);
display:flex; align-items:center; justify-content:center;
color:var(--sgis-text-2); margin-bottom:4px;
}
.sgis-review-empty-icon svg { width:20px; height:20px; }
/* === 跨区送礼推荐 === */
.sgis-gift-card {
padding:10px; border-radius:8px; background:linear-gradient(135deg, rgba(167,139,250,0.12), rgba(102,192,244,0.08));
border:1px solid rgba(167,139,250,0.2); margin-top:8px;
}
.sgis-gift-title { font-size:12px; font-weight:700; margin-bottom:6px; display:flex; align-items:center; gap:4px; }
.sgis-gift-scheme { font-size:14px; font-weight:700; color:#66c0f4; margin:4px 0; }
.sgis-gift-save { font-size:20px; font-weight:800; color:#4bb54f; }
.sgis-gift-pct { font-size:11px; color:#4bb54f; font-weight:600; }
.sgis-gift-prices { display:flex; gap:12px; margin:6px 0; font-size:10px; }
.sgis-gift-price-item { display:flex; flex-direction:column; }
.sgis-gift-price-label { color:var(--sgis-text-2); }
.sgis-gift-price-val { font-weight:600; }
.sgis-gift-warn { font-size:9px; color:#e6a839; margin-top:6px; padding:4px 6px; background:rgba(230,168,57,0.08); border-radius:4px; }
.sgis-gift-rule { font-size:9px; color:var(--sgis-text-2); margin-top:4px; }
.sgis-gift-alt { font-size:10px; color:var(--sgis-text-2); margin-top:6px; padding-top:6px; border-top:1px dashed rgba(255,255,255,0.06); }
/* === v2.9.9: SVG 价格走势图 === */
.sgis-price-chart-wrap { margin-top:8px; border-radius:8px; background:rgba(255,255,255,0.02); padding:8px 6px 4px; border:1px solid rgba(255,255,255,0.05); }
.sgis-price-chart-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:4px; }
.sgis-price-chart-title { font-size:10px; color:var(--sgis-text-2); font-weight:500; }
.sgis-chart-range-group { display:flex; gap:2px; }
.sgis-chart-range-btn {
font-size:9px; padding:1px 6px; border-radius:8px; border:1px solid rgba(255,255,255,0.08);
background:transparent; color:var(--sgis-text-2); cursor:pointer; transition:all 0.15s;
}
.sgis-chart-range-btn:hover { background:rgba(255,255,255,0.06); }
.sgis-chart-range-btn.active { background:rgba(102,192,244,0.2); color:#66c0f4; border-color:rgba(102,192,244,0.4); }
/* === v2.9.9: DRM 警告徽章 === */
.sgis-drm-badge {
display:inline-flex; align-items:center; gap:3px; font-size:10px; font-weight:600;
padding:2px 8px; border-radius:4px; background:rgba(239,68,68,0.15);
color:#f87171; border:1px solid rgba(239,68,68,0.25);
}
.sgis-drm-badge.warn { background:rgba(245,158,11,0.15); color:#fbbf24; border-color:rgba(245,158,11,0.25); }
/* === v2.9.9: 中文语音标记 === */
.sgis-audio-badge {
display:inline-flex; align-items:center; gap:3px; font-size:10px; font-weight:600;
padding:2px 8px; border-radius:4px; background:rgba(75,181,79,0.15);
color:#4bb54f; border:1px solid rgba(75,181,79,0.25);
}
/* === v2.9.93: 资料受限/Steam正在了解 状态徽章 (参考 steam-restricted-game-realtime-userscript) === */
.sgis-profile-badge {
display:inline-flex; align-items:center; gap:3px; font-size:10px; font-weight:600;
padding:2px 8px; border-radius:4px; border:1px solid rgba(255,255,255,0.12);
background:rgba(255,255,255,0.06); color:var(--sgis-text);
}
.sgis-profile-badge svg { width:12px; height:12px; flex-shrink:0; }
.sgis-profile-badge.restricted { background:rgba(239,68,68,0.15); color:#f87171; border-color:rgba(239,68,68,0.25); }
.sgis-profile-badge.learning { background:rgba(245,158,11,0.15); color:#fbbf24; border-color:rgba(245,158,11,0.25); }
.sgis-profile-badge.normal { background:rgba(34,197,94,0.12); color:#22c55e; border-color:rgba(34,197,94,0.2); }
.sgis-profile-badge.loading { background:rgba(255,255,255,0.04); color:var(--sgis-text-2); border-color:rgba(255,255,255,0.08); }
/* === v2.9.11: gamestatus.info 破解状态徽章 === */
.sgis-crack-badge {
display:inline-flex; align-items:center; gap:3px; font-size:10px; font-weight:600;
padding:2px 8px; border-radius:4px; border:1px solid rgba(255,255,255,0.12);
background:rgba(255,255,255,0.06); color:var(--sgis-text);
}
.sgis-crack-badge svg { width:12px; height:12px; flex-shrink:0; }
.sgis-crack-badge.cracked { background:rgba(34,197,94,0.15); color:#22c55e; border-color:rgba(34,197,94,0.25); }
.sgis-crack-badge.not-cracked { background:rgba(239,68,68,0.15); color:#f87171; border-color:rgba(239,68,68,0.25); }
.sgis-crack-badge.bypass { background:rgba(245,158,11,0.15); color:#fbbf24; border-color:rgba(245,158,11,0.25); }
.sgis-crack-badge.release-today { background:rgba(59,130,246,0.15); color:#60a5fa; border-color:rgba(59,130,246,0.25); }
.sgis-crack-badge.aaa { background:rgba(168,85,247,0.15); color:#c084fc; border-color:rgba(168,85,247,0.25); }
.sgis-crack-protection {
display:inline-flex; align-items:center; font-size:9px; font-weight:500;
padding:1px 6px; border-radius:3px; background:rgba(255,255,255,0.05);
color:var(--sgis-text-2); border:1px solid rgba(255,255,255,0.08);
}
.sgis-crack-group {
display:inline-flex; align-items:center; font-size:9px; font-weight:500;
padding:1px 6px; border-radius:3px; background:rgba(20,184,166,0.12);
color:#2dd4bf; border:1px solid rgba(20,184,166,0.2);
}
.sgis-crack-date { font-size:9px; color:var(--sgis-text-2); }
.sgis-crack-score {
display:inline-flex; align-items:center; gap:2px; font-size:9px; font-weight:600;
padding:1px 6px; border-radius:3px; background:rgba(250,204,21,0.12);
color:#facc15; border:1px solid rgba(250,204,21,0.2);
}
.sgis-crack-score svg { width:10px; height:10px; }
.sgis-crack-hardware {
margin-top:6px; padding:6px 8px; border-radius:6px;
background:rgba(255,255,255,0.03); border:1px solid rgba(255,255,255,0.06);
}
.sgis-crack-hardware-title { font-size:10px; font-weight:600; color:var(--sgis-text-2); margin-bottom:4px; }
.sgis-crack-hw-row { display:flex; gap:6px; font-size:10px; line-height:1.5; }
.sgis-crack-hw-key { color:var(--sgis-text-2); min-width:32px; font-weight:500; }
.sgis-crack-hw-val { color:var(--sgis-text); flex:1; word-break:break-word; }
/* === v2.9.15: 评测搜索(flex 布局,按钮不再溢出) === */
.sgis-review-search-wrap { display:flex; align-items:center; gap:6px; margin-bottom:6px; }
.sgis-review-search-input {
flex:1; min-width:0; width:auto; box-sizing:border-box;
font-size:11px; padding:5px 9px; border-radius:6px;
border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.03);
color:var(--sgis-text); transition:border-color 0.15s;
}
.sgis-review-search-input:focus { outline:none; border-color:rgba(102,192,244,0.4); }
.sgis-review-search-input::placeholder { color:var(--sgis-text-2); font-size:10px; }
.sgis-review-regex-toggle {
flex-shrink:0; font-size:10px; padding:4px 8px; border-radius:6px;
border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.03);
color:var(--sgis-text-2); cursor:pointer; transition:all 0.15s;
font-family:monospace; font-weight:600; letter-spacing:0.5px;
}
.sgis-review-regex-toggle:hover { background:rgba(167,139,250,0.1); border-color:rgba(167,139,250,0.3); }
.sgis-review-regex-toggle.active { background:rgba(167,139,250,0.2); color:#a78bfa; border-color:rgba(167,139,250,0.4); box-shadow:0 0 0 1px rgba(167,139,250,0.2); }
.sgis-review-search-clear {
flex-shrink:0; width:22px; height:22px; padding:0; border-radius:50%;
border:1px solid rgba(255,255,255,0.1); background:rgba(255,255,255,0.03);
color:var(--sgis-text-2); cursor:pointer; transition:all 0.15s;
display:flex; align-items:center; justify-content:center; font-size:13px; line-height:1;
}
.sgis-review-search-clear:hover { background:rgba(230,57,70,0.15); color:#e63946; border-color:rgba(230,57,70,0.4); }`;
const CSS_LOAD =` /* === 加载容器(替代裸 spinner,给用户结构感) === */
.sgis-load-block { display:flex; flex-direction:column; align-items:stretch; justify-content:flex-start; padding:18px 16px 14px; gap:10px; }
.sgis-load-head { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--sgis-text); }
.sgis-load-head .sgis-spinner { width:18px; height:18px; margin:0; flex-shrink:0; }
.sgis-load-stage { flex:1; font-weight:600; letter-spacing:0.2px; }
.sgis-load-pct { font-size:11px; color:var(--sgis-text-2); font-variant-numeric:tabular-nums; flex-shrink:0; }
/* === 主进度条(带渐变 + 流光) === */
.sgis-load-bar { position:relative; height:6px; background:rgba(255,255,255,0.05); border-radius:3px; overflow:hidden; box-shadow:inset 0 1px 2px rgba(0,0,0,0.3); }
.sgis-load-bar-fill { position:absolute; left:0; top:0; bottom:0; width:0%; border-radius:3px; background:linear-gradient(90deg,#06b6d4,#3b82f6,#8b5cf6); transition:width 0.35s cubic-bezier(0.4,0,0.2,1); box-shadow:0 0 10px rgba(59,130,246,0.5); }
.sgis-load-bar-fill::after { content:""; position:absolute; inset:0; background:linear-gradient(90deg,transparent,rgba(255,255,255,0.45),transparent); animation:sgis-load-shimmer 1.4s linear infinite; }
@keyframes sgis-load-shimmer { 0%{transform:translateX(-100%)} 100%{transform:translateX(100%)} }
/* === 阶段指示条(4 段小条,阶段切换时高亮) === */
.sgis-load-stages { display:flex; gap:3px; }
.sgis-load-stage-bar { flex:1; height:3px; border-radius:2px; background:rgba(255,255,255,0.06); transition:all 0.3s; }
.sgis-load-stage-bar.active { background:linear-gradient(90deg,#06b6d4,#3b82f6); box-shadow:0 0 6px rgba(59,130,246,0.6); }
.sgis-load-stage-bar.done { background:rgba(16,185,129,0.55); }
/* === 骨架屏:与真实游戏行 1:1 布局,避免加载完成后跳变 === */
.sgis-skel-list { display:flex; flex-direction:column; gap:6px; margin-top:4px; }
.sgis-skel-row { display:flex; align-items:center; gap:10px; padding:8px 10px; background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.04); border-radius:8px; }
.sgis-skel-icon { width:32px; height:32px; border-radius:4px; flex-shrink:0; background:linear-gradient(90deg,rgba(139,92,246,0.08),rgba(139,92,246,0.18),rgba(139,92,246,0.08)); background-size:200% 100%; animation:sgis-skel-shimmer 1.4s linear infinite; }
.sgis-skel-lines { flex:1; min-width:0; display:flex; flex-direction:column; gap:6px; }
.sgis-skel-line { height:8px; border-radius:3px; background:linear-gradient(90deg,rgba(255,255,255,0.04),rgba(255,255,255,0.10),rgba(255,255,255,0.04)); background-size:200% 100%; animation:sgis-skel-shimmer 1.4s linear infinite; }
.sgis-skel-line.l1 { width:55%; }
.sgis-skel-line.l2 { width:30%; height:7px; opacity:0.6; }
.sgis-skel-row:nth-child(odd) .sgis-skel-icon, .sgis-skel-row:nth-child(odd) .sgis-skel-line { animation-delay:0s; }
.sgis-skel-row:nth-child(even) .sgis-skel-icon, .sgis-skel-row:nth-child(even) .sgis-skel-line { animation-delay:0.2s; }
.sgis-skel-row:nth-child(3n) .sgis-skel-icon, .sgis-skel-row:nth-child(3n) .sgis-skel-line { animation-delay:0.4s; }
@keyframes sgis-skel-shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
/* === 加载中已完成计数(用户能看到 "已处理 X / 约 Y" 真实进度) === */
.sgis-load-counter { font-size:10px; color:var(--sgis-text-2); text-align:center; font-variant-numeric:tabular-nums; margin-top:2px; }`;
// v2.11.40: 主题层——原 CSS_SGIS_OVERRIDE(无条件注入的紫色全局覆盖)重构为典雅紫主题层,
// 由 applySglvTheme() 按用户主题动态注入,不再进入 CSS_INLINE。内容保持不变(默认主题零视觉回归)。
const CSS_THEME_PURPLE =` /* ====== 1. 全局 CSS 变量覆盖 → 紫色主调 ====== */
:root {
--sglv-accent: #a78bfa !important;
--sglv-accent-light: #c4b5fd !important;
--sglv-accent-deep: #8b5cf6 !important;
--sglv-accent-glow: rgba(139, 92, 246, 0.4) !important;
--sglv-accent-bg: rgba(139, 92, 246, 0.12) !important;
--sglv-accent-border: rgba(139, 92, 246, 0.3) !important;
}
/* ====== 2. 面板头部 → 紫色调 ====== */
.sglv-panel > .sglv-header {
background: linear-gradient(135deg, rgba(27, 40, 56, 0.95) 0%, rgba(42, 71, 94, 0.95) 100%) !important;
border-bottom: 1px solid rgba(139, 92, 246, 0.2) !important;
}
.sglv-header-brand {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.18), rgba(99, 102, 241, 0.12)) !important;
border: 1px solid rgba(139, 92, 246, 0.25) !important;
color: #a78bfa !important;
}
.sglv-header-brand:hover {
box-shadow: 0 2px 8px rgba(139, 92, 246, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.1) !important;
}
/* ====== 3. Tab 激活态 → 紫色 ====== */
.sglv-panel .sglv-tab.active {
color: #fff !important;
background: rgba(139, 92, 246, 0.18) !important;
border-color: rgba(139, 92, 246, 0.4) !important;
box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.15) inset, 0 1px 4px rgba(139, 92, 246, 0.2) !important;
}
.sglv-panel .sglv-tab:hover {
background: rgba(139, 92, 246, 0.08) !important;
}
/* ====== 4. 全局搜索框聚焦 → 紫色 ====== */
.sglv-panel .sglv-global-search:focus-within {
border-color: rgba(139, 92, 246, 0.55) !important;
box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.25) !important;
}
/* ====== 5. KPI 卡片 — 恢复紫色变体 ====== */
/* amber 变体保持金色(原版设计) */
.sglv-playtime-kpi-card.amber {
border-color: rgba(245, 158, 11, 0.35) !important;
}
.sglv-playtime-kpi-card.amber::before { background: #f59e0b !important; }
.sglv-playtime-kpi-card.amber .sglv-playtime-kpi-value { color: #fbbf24 !important; }
.sglv-playtime-kpi-card.amber .sglv-playtime-kpi-icon {
background: rgba(245, 158, 11, 0.15) !important;
color: #fbbf24 !important;
}
/* purple 变体 → 恢复紫色 */
.sglv-playtime-kpi-card.purple {
border-color: rgba(139, 92, 246, 0.35) !important;
}
.sglv-playtime-kpi-card.purple::before { background: #8b5cf6 !important; }
.sglv-playtime-kpi-card.purple .sglv-playtime-kpi-value { color: #c4b5fd !important; }
.sglv-playtime-kpi-card.purple .sglv-playtime-kpi-icon {
background: rgba(139, 92, 246, 0.15) !important;
color: #c4b5fd !important;
}
/* blue/cyan/green/rose 语义色保持不变 */
/* ====== 6. 统计仪表 — 区块标题 → 紫色 ====== */
.sglv-playtime-section-title {
color: #a78bfa !important;
border-bottom: 1px solid rgba(139, 92, 246, 0.12) !important;
}
.sglv-playtime-section-title svg {
color: #a78bfa !important;
}
/* ====== 7. 趋势筛选按钮 → 紫色激活 ====== */
.sglv-trend-btn.active {
background: rgba(139, 92, 246, 0.18) !important;
color: #c4b5fd !important;
border-color: rgba(139, 92, 246, 0.3) !important;
}
.sglv-trend-btn:hover {
background: rgba(139, 92, 246, 0.1) !important;
}
/* ====== 8. 进度条 → 青蓝紫渐变 ====== */
#sglv-stage-progress .sglv-stage-progress-fill {
background: linear-gradient(90deg, #06b6d4 0%, #3b82f6 50%, #8b5cf6 100%) !important;
}
#sglv-stage-progress {
background: linear-gradient(180deg, rgba(139, 92, 246, 0.06), transparent) !important;
border-bottom: 1px solid rgba(139, 92, 246, 0.12) !important;
}
/* ====== 9. 仪表盘 — 新增雷达图区块 ====== */
.sglv-radar-row {
display: grid;
grid-template-columns: 280px 1fr;
gap: 10px;
flex: 0 0 auto;
min-height: 0;
}
@media (max-width: 900px) {
.sglv-radar-row { grid-template-columns: 1fr; }
}
.sglv-radar-card {
background: linear-gradient(135deg, rgba(34, 211, 238, 0.05), rgba(139, 92, 246, 0.04), rgba(244, 114, 182, 0.04), rgba(13, 17, 23, 0.4));
border: 1px solid rgba(139, 92, 246, 0.12);
border-radius: 10px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.sglv-radar-card-title {
font-size: 12px;
font-weight: 700;
background: linear-gradient(90deg, #22d3ee, #34d399, #a78bfa, #f472b6, #f59e0b);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 6px;
align-self: flex-start;
}
.sglv-radar-card-title svg { width: 14px; height: 14px; color: #a78bfa; }
.sglv-radar-svg { width: 220px; height: 220px; }
.sglv-radar-svg .radar-grid { fill: none; stroke: rgba(255, 255, 255, 0.08); stroke-width: 1; }
.sglv-radar-svg .radar-axis { stroke-width: 1; }
.sglv-radar-svg .radar-area { stroke-width: 2; stroke-linejoin: round; transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
.sglv-radar-svg .radar-point { stroke: #0d1117; stroke-width: 2; transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
.sglv-radar-svg .radar-label { font-size: 10px; font-weight: 700; text-anchor: middle; }
.sglv-radar-svg .radar-value { font-size: 11px; font-weight: 800; text-anchor: middle; }
/* 雷达图右侧统计面板 */
.sglv-radar-stats {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px;
background: rgba(13, 17, 23, 0.4);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
.sglv-radar-stat-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
transition: background 0.15s;
}
.sglv-radar-stat-item:hover { background: rgba(255, 255, 255, 0.05); }
.sglv-radar-stat-icon {
width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.sglv-radar-stat-icon svg { width: 14px; height: 14px; }
.sglv-radar-stat-info { flex: 1; min-width: 0; }
.sglv-radar-stat-label { font-size: 10px; color: #8b949e; font-weight: 600; }
.sglv-radar-stat-bar {
height: 4px; background: rgba(255, 255, 255, 0.06); border-radius: 2px;
overflow: hidden; margin-top: 3px;
}
.sglv-radar-stat-bar-fill {
height: 100%; border-radius: 2px;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.sglv-radar-stat-val {
font-size: 14px; font-weight: 700;
font-variant-numeric: tabular-nums; flex-shrink: 0;
}
/* ====== 10. 仪表盘 KPI 行增强 — hover 微光 ====== */
.sglv-playtime-kpi-card {
position: relative;
transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
}
.sglv-playtime-kpi-card:hover {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
}
.sglv-playtime-kpi-card.amber:hover {
box-shadow: 0 3px 10px rgba(245, 158, 11, 0.15);
}
/* ====== 11. 仪表盘 KPI 行 — 7 列 grid (含雷达图入口) ====== */
.sglv-stats-dashboard {
display: grid !important;
grid-template-columns: repeat(7, 1fr) !important;
gap: 6px !important;
}
/* ====== 12. 甜甜圈图中心文字 → 紫色 ====== */
.sglv-pt-donut-center-val { fill: #a78bfa !important; }
.sglv-pt-donut-center-label { fill: #8b949e !important; }
/* ====== 13. 热力图标题 → 紫色 ====== */
.sglv-heatmap-section .sglv-playtime-section-title { color: #a78bfa !important; }
.sglv-hm-mini-title { color: #a78bfa !important; }
/* ====== 14. 成就页 — 环形进度图背景 → 紫色 ====== */
.sglv-ach-ring-bg { stroke: rgba(139, 92, 246, 0.1) !important; }
/* 注意: gradient 定义在 SVG defs 中,需在 JS 中修改 stop colors */
/* ====== 15. 成就页 — 侧边栏用户卡片紫色调 ====== */
.sglv-ach-user-card {
background: linear-gradient(160deg, rgba(139, 92, 246, 0.1), rgba(99, 102, 241, 0.04)) !important;
}
.sglv-ach-user-card::after {
background: radial-gradient(circle, rgba(139, 92, 246, 0.12), transparent 60%) !important;
}
.sglv-ach-user-avatar {
background: linear-gradient(135deg, #8b5cf6, #6366f1) !important;
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.35) !important;
}
.sglv-ach-user-level {
border: 2px solid #a78bfa !important;
color: #c4b5fd !important;
}
.sglv-ach-user-pts strong { color: #c4b5fd !important; }
/* ====== 16. 成就页 — 分类导航紫色激活 ====== */
.sglv-ach-nav-item:hover { background: rgba(139, 92, 246, 0.08) !important; }
.sglv-ach-nav-item.active {
background: linear-gradient(90deg, rgba(139, 92, 246, 0.2), transparent) !important;
}
.sglv-ach-nav-item-icon { background: rgba(139, 92, 246, 0.12) !important; }
.sglv-ach-nav-item.active .sglv-ach-nav-item-icon {
background: rgba(139, 92, 246, 0.2) !important;
}
.sglv-ach-nav-item-badge {
background: rgba(139, 92, 246, 0.15) !important;
color: #c4b5fd !important;
}
.sglv-ach-nav-item.active .sglv-ach-nav-item-badge {
background: #8b5cf6 !important;
color: #0d1117 !important;
}
/* ====== 17. 成就页 — 侧边栏最近解锁紫色调 ====== */
.sglv-ach-sidebar-recent-icon {
background: linear-gradient(135deg, #8b5cf6, #6366f1) !important;
}
.sglv-ach-sidebar-recent-item:hover { background: rgba(139, 92, 246, 0.06) !important; }
.sglv-ach-sidebar-recent-name { color: #e2e8f0 !important; }
/* ====== 18. 成就页 — 统计概览区紫色调 ====== */
.sglv-ach-header {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.6), rgba(15, 23, 42, 0.6)) !important;
border-bottom: 1px solid rgba(139, 92, 246, 0.12) !important;
gap: 14px !important;
padding: 16px 20px !important;
}
/* v2.9.99: 环形进度数值使用炫彩渐变,不强制紫色 */
/* ====== 19. 成就页 — 成就卡片紫色调 ====== */
.sglv-ach-card {
background: linear-gradient(135deg, rgba(13, 17, 23, 0.6), rgba(30, 41, 59, 0.4)) !important;
border: 1px solid rgba(139, 92, 246, 0.08) !important;
}
.sglv-ach-card:hover {
border-color: rgba(139, 92, 246, 0.3) !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3) !important;
}
.sglv-ach-card.unlocked {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.05), rgba(13, 17, 23, 0.4)) !important;
border-color: rgba(139, 92, 246, 0.15) !important;
}
.sglv-ach-card.unlocked .sglv-ach-card-name { color: #c4b5fd !important; }
.sglv-ach-card.unlocked .sglv-ach-card-pts {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(99, 102, 241, 0.1)) !important;
border-color: rgba(139, 92, 246, 0.3) !important;
color: #c4b5fd !important;
}
.sglv-ach-card.unlocked .sglv-ach-card-progress-fill {
background: linear-gradient(90deg, #22d3ee, #34d399, #fbbf24) !important;
box-shadow: 0 0 8px rgba(34, 211, 238, 0.4) !important;
}
.sglv-ach-card.locked .sglv-ach-card-progress-fill {
background: linear-gradient(90deg, #38bdf8, #a78bfa, #f59e0b) !important;
box-shadow: 0 0 8px rgba(56, 189, 248, 0.25) !important;
}
.sglv-ach-card-icon {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(99, 102, 241, 0.08)) !important;
}
/* 稀有度色保持不变(legendary 已是金色, common/rare/epic 语义色保留) */
.sglv-ach-card.unlocked .sglv-ach-card-icon {
box-shadow: inset 0 1px 0 rgba(255,255,255,0.15), inset 0 -1px 0 rgba(0,0,0,0.2), 0 4px 12px rgba(139, 92, 246, 0.2) !important;
}
/* ====== 20. 成就页 — 内容标题紫色调 ====== */
.sglv-ach-content-title { color: #a78bfa !important; }
.sglv-ach-content-count {
background: rgba(139, 92, 246, 0.1) !important;
color: #c4b5fd !important;
}
.sglv-ach-reset-btn {
border: 1px solid rgba(239, 68, 68, 0.3) !important;
background: rgba(239, 68, 68, 0.08) !important;
}
/* ====== 21. 个人展柜 — 英雄区紫青色调(恢复原版色彩平衡) ====== */
.sglv-sc-hero {
background: linear-gradient(135deg, rgba(99,102,241,0.10) 0%, rgba(168,85,247,0.06) 50%, rgba(6,182,212,0.04) 100%) !important;
border: 1px solid rgba(99,102,241,0.15) !important;
}
.sglv-sc-hero::before {
background: radial-gradient(ellipse at top right, rgba(168,85,247,0.10), transparent 60%) !important;
}
.sglv-sc-avatar {
border: 2px solid rgba(255, 255, 255, 0.12) !important;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(168, 85, 247, 0.25) !important;
}
.sglv-sc-avatar-placeholder {
background: linear-gradient(135deg, #6366f1, #8b5cf6) !important;
}
/* 用户名保持白色,不加金色 */
.sglv-sc-hero-name { color: #f1f5f9 !important; }
/* 国家标签保持中性灰 */
.sglv-sc-hero-country {
background: rgba(148, 163, 184, 0.08) !important;
color: #94a3b8 !important;
border: 1px solid rgba(148, 163, 184, 0.1) !important;
}
/* Hero 统计图标 → 恢复靛蓝色 */
.sglv-sc-hero-stat svg { color: #818cf8 !important; }
/* Hero 统计数值 → 恢复浅色 */
.sglv-sc-hero-stat-val { color: #e2e8f0 !important; }
/* ====== 22. 展柜卡片标题 → 恢复紫色 ====== */
.sglv-sc-card-header svg { color: #a78bfa !important; }
.sglv-sc-card-title { color: #e6edf3 !important; }
/* ====== 23. 展柜封面网格 → 恢复紫色 hover ====== */
.sglv-sc-cover-item {
border: 1px solid rgba(255, 255, 255, 0.06) !important;
}
.sglv-sc-cover-item:hover {
border-color: rgba(168, 85, 247, 0.5) !important;
box-shadow: 0 4px 16px rgba(168, 85, 247, 0.2) !important;
}
/* ====== 24. 展柜最近游玩 → 恢复靛蓝紫色 ====== */
.sglv-sc-playtime-item:hover { background: rgba(99, 102, 241, 0.06) !important; }
.sglv-sc-playtime-bar { background: rgba(99, 102, 241, 0.1) !important; }
.sglv-sc-playtime-bar-fill {
background: linear-gradient(90deg, #6366f1, #a855f7) !important;
}
/* ====== 25. 展柜标签徽章 → 移除金色覆盖,恢复原版多彩 ====== */
/* 原版已有: collector(紫), playtime(青), mastery(绿), diversity(橙), loyalty(粉), special(金) */
/* ====== 27. Toast 通知 → 紫色 ====== */
#sglv-toast.sglv-toast-success { border-left-color: #8b5cf6 !important; }
#sglv-toast.sglv-toast-info { border-left-color: #66c0f4 !important; }
/* ====== 28. 页码按钮 → 紫色 hover ====== */
.sglv-page-arrow:hover:not(:disabled) {
background: rgba(139, 92, 246, 0.15) !important;
border-color: rgba(139, 92, 246, 0.4) !important;
}
/* ====== 29. 仪表盘图表行卡片容器 → 紫色微边 ====== */
.sglv-playtime-col {
background: rgba(13, 17, 23, 0.3);
border: 1px solid rgba(139, 92, 246, 0.06);
border-radius: 10px;
padding: 10px;
}
.sglv-heatmap-section,
.sglv-heatmap-mini-section {
background: rgba(13, 17, 23, 0.3) !important;
border: 1px solid rgba(139, 92, 246, 0.06) !important;
border-radius: 10px !important;
}
/* ====== 30. SGIS 侧边栏标签 → 恢复紫色激活态 ====== */
.sgis-tab.active .sgis-svg {
filter: drop-shadow(0 1px 4px rgba(139, 92, 246, 0.45)) !important;
}
.sgis-tab.active {
border-bottom-color: #8b5cf6 !important;
}
.sgis-tabs::after {
background: linear-gradient(90deg, transparent, rgba(139, 92, 246, 0.35), transparent) !important;
}
/* ====== 31. SGIS 标题图标 → 恢复紫蓝渐变 ====== */
.sgis-title-icon {
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 50%, #3b82f6 100%) !important;
box-shadow:
0 4px 12px rgba(139, 92, 246, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.25),
inset 0 -1px 0 rgba(0, 0, 0, 0.2) !important;
}
.sgis-fab {
background: linear-gradient(135deg, #8b5cf6, #6366f1) !important;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.4) !important;
}
/* ====== 32. 加载进度条 → 恢复青蓝紫渐变 ====== */
.sgis-load-bar-fill {
background: linear-gradient(90deg, #06b6d4, #3b82f6, #8b5cf6) !important;
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5) !important;
}
.sgis-load-stage-bar.active {
background: linear-gradient(90deg, #06b6d4, #3b82f6) !important;
box-shadow: 0 0 6px rgba(59, 130, 246, 0.6) !important;
}
.sgis-skel-icon {
background: linear-gradient(90deg, rgba(139, 92, 246, 0.08), rgba(139, 92, 246, 0.18), rgba(139, 92, 246, 0.08)) !important;
background-size: 200% 100% !important;
}
/* ====== 34. 云存档 KPI 卡片 → 紫色调 ====== */
.sglv-cs-source-badge {
background: rgba(139, 92, 246, 0.12) !important;
color: #c4b5fd !important;
border: 1px solid rgba(139, 92, 246, 0.2) !important;
}
.sglv-cs-refresh-btn:hover {
color: #a78bfa !important;
background: rgba(139, 92, 246, 0.12) !important;
}
.sglv-cs-top10-bar {
background: linear-gradient(90deg, #6366f1, #a855f7) !important;
}
/* ====== 35. 集合工具栏刷新按钮 → 紫色 hover ====== */
.sglv-col-refresh-btn:hover {
color: #a78bfa !important;
background: rgba(139, 92, 246, 0.1) !important;
}
/* ====== 36. 移动端适配 ====== */
@media (max-width: 768px) {
.sglv-radar-row { grid-template-columns: 1fr; }
.sglv-radar-svg { width: 180px; height: 180px; }
}
@media (max-width: 480px) {
.sglv-radar-svg { width: 160px; height: 160px; }
.sglv-radar-card { padding: 8px; }
}
/* ====== 37. v2.9.97: SGIS Hero — 紫青平衡 + 紧凑布局 + 保留背景图 ====== */
/* 主卡片:恢复紫色渐变基调,紧凑 padding 保留 */
.sgis-profile-main-card {
background: linear-gradient(160deg, #1a1633 0%, #16142b 40%, #0f0d20 100%) !important;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(139, 92, 246, 0.15) !important;
padding: 14px 14px 12px !important;
border-radius: 16px !important;
}
/* 右上角径向光晕 → 紫色 */
.sgis-profile-main-card::before {
background: radial-gradient(circle, rgba(139, 92, 246, 0.4) 0%, transparent 60%) !important;
}
/* 底部高光线 → 紫色 */
.sgis-profile-main-card::after {
background: linear-gradient(90deg, transparent 0%, rgba(139, 92, 246, 0.6) 50%, transparent 100%) !important;
}
/* 背景装饰图 → 恢复原始 game-library.png + 紫色滤镜 + mask */
.sgis-profile-main-card-bg {
background: url('https://leanisssharedstorage.blob.core.windows.net/copilot/asdm-files/game-library.png') no-repeat center center !important;
background-size: contain !important;
opacity: 0.18 !important;
filter: saturate(1.2) drop-shadow(0 0 20px rgba(139, 92, 246, 0.25)) !important;
mask-image: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 40%, rgba(0,0,0,1) 100%) !important;
-webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 40%, rgba(0,0,0,1) 100%) !important;
}
/* Hero 头部行 → 紧凑(保留) */
.sgis-profile-head {
gap: 12px !important;
margin-bottom: 12px !important;
}
/* 头像 → 64px + 青色边框(原版色调) */
.sgis-profile-avatar {
width: 64px !important;
height: 64px !important;
border-radius: 18px !important;
border: 2px solid rgba(102, 192, 244, 0.5) !important;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4), 0 0 0 3px rgba(102, 192, 244, 0.08) !important;
}
/* 在线状态徽章 → 恢复紫蓝渐变 */
.sgis-profile-avatar-badge {
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 50%, #3b82f6 100%) !important;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.5), 0 0 0 2px #16142b !important;
}
/* 等级徽章 → 保持金色(原版即为金色,作为 premium accent) */
.sgis-profile-level-badge {
background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%) !important;
box-shadow: 0 2px 6px rgba(249, 115, 22, 0.4) !important;
}
/* 用户名 → 白色,不加金色 text-shadow */
.sgis-profile-name {
text-shadow: none !important;
}
/* SteamID → 恢复灰蓝色 */
.sgis-profile-steamid {
color: rgba(148, 163, 184, 0.9) !important;
}
/* 验证徽章 → 恢复紫蓝渐变 */
.sgis-profile-verified {
background: linear-gradient(135deg, #8b5cf6, #3b82f6) !important;
box-shadow: 0 2px 6px rgba(139, 92, 246, 0.4) !important;
}
/* 好友数进度环 → 恢复绿色 */
.sgis-friend-gauge-text b { color: #10b981 !important; }
.sgis-friend-gauge:hover { background: rgba(255, 255, 255, 0.05) !important; }
/* ====== 38. SGIS KPI 统计卡片 — 恢复多彩语义色 ====== */
/* 默认状态:恢复中性背景,移除大面积金色 */
.sgis-profile-stat {
background: linear-gradient(160deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.01) 100%) !important;
border: 1px solid rgba(255, 255, 255, 0.06) !important;
border-radius: 12px !important;
transition: all 0.2s ease !important;
}
/* 顶部高光线 → 恢复 currentColor 语义色 */
.sgis-profile-stat::after {
opacity: 0.35 !important;
}
/* hover → 紫色光晕 + 上浮 */
.sgis-profile-stat:hover {
border-color: rgba(139, 92, 246, 0.3) !important;
background: linear-gradient(160deg, rgba(139, 92, 246, 0.08) 0%, rgba(59, 130, 246, 0.03) 100%) !important;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3) !important;
transform: translateY(-2px) !important;
}
/* 默认图标背景 → 恢复紫蓝渐变 */
.sgis-profile-stat-icon {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(59, 130, 246, 0.08)) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04) !important;
}
.sgis-profile-stat-icon svg { color: #c4b5fd !important; }
/* t-blue / t-purple / t-cyan / t-green / t-rose → 全部恢复原版语义色 */
.sgis-profile-stat-icon.t-blue {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.22), rgba(37, 99, 235, 0.08)) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04) !important;
}
.sgis-profile-stat-icon.t-blue svg { color: #93c5fd !important; }
.sgis-profile-stat-icon.t-purple {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.22), rgba(139, 92, 246, 0.08)) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04) !important;
}
.sgis-profile-stat-icon.t-purple svg { color: #c4b5fd !important; }
.sgis-profile-stat-val.t-purple { color: #c4b5fd !important; }
/* KPI 值文字 → 移除金色光泽 */
.sgis-profile-stat-val { text-shadow: none !important; }
/* 5列模式 KPI 紧凑(保留布局优化) */
.sgis-profile-stats.cols-5 { gap: 6px !important; }
.sgis-profile-stats.cols-5 .sgis-profile-stat {
padding: 10px 4px 8px !important;
border-radius: 10px !important;
}
/* ====== 39. SGIS 区块标题 — 恢复紫色 ====== */
.sgis-section-title {
color: #a78bfa !important;
border-bottom: 1px solid rgba(139, 92, 246, 0.08) !important;
}
.sgis-section-title svg { color: #a78bfa !important; }
/* ====== 40. SGIS 战绩/游戏人生 双大卡 — 恢复紫蓝色调 ====== */
.sgis-feature-card {
background: linear-gradient(160deg, rgba(139, 92, 246, 0.12) 0%, rgba(59, 130, 246, 0.04) 50%, rgba(15, 17, 35, 0.6) 100%) !important;
border: 1px solid rgba(139, 92, 246, 0.2) !important;
transition: all 0.25s ease !important;
}
.sgis-feature-card::before {
background: radial-gradient(circle, rgba(139, 92, 246, 0.2) 0%, transparent 60%) !important;
}
.sgis-feature-card::after {
background: linear-gradient(90deg, transparent 0%, rgba(139, 92, 246, 0.5) 50%, transparent 100%) !important;
}
.sgis-feature-card:hover {
border-color: rgba(139, 92, 246, 0.4) !important;
box-shadow: 0 8px 20px rgba(139, 92, 246, 0.2) !important;
transform: translateY(-2px) !important;
}
/* alt 变体 → 恢复靛蓝色 */
.sgis-feature-card.alt {
background: linear-gradient(160deg, rgba(99, 102, 241, 0.12) 0%, rgba(59, 130, 246, 0.04) 50%, rgba(15, 17, 35, 0.6) 100%) !important;
border-color: rgba(99, 102, 241, 0.2) !important;
}
.sgis-feature-card.alt::before {
background: radial-gradient(circle, rgba(99, 102, 241, 0.2) 0%, transparent 60%) !important;
}
.sgis-feature-card.alt::after {
background: linear-gradient(90deg, transparent 0%, rgba(99, 102, 241, 0.5) 50%, transparent 100%) !important;
}
/* 特征卡片图标 → 恢复紫蓝色 */
.sgis-feature-icon {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.22), rgba(59, 130, 246, 0.12)) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04), 0 2px 6px rgba(139, 92, 246, 0.15) !important;
}
.sgis-feature-icon svg { color: #c4b5fd !important; }
.sgis-feature-card.alt .sgis-feature-icon {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.22), rgba(59, 130, 246, 0.12)) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04), 0 2px 6px rgba(99, 102, 241, 0.15) !important;
}
.sgis-feature-card.alt .sgis-feature-icon svg { color: #a5b4fc !important; }
/* hover 箭头 → 紫色 */
.sgis-feature-card:hover .sgis-feature-arrow { color: #c4b5fd !important; }
/* 特征卡片数值 → 恢复紫色 */
.sgis-feature-stat-val.t-purple { color: #c4b5fd !important; }
/* ====== 40b. SGIS 双大卡紧凑优化 — 3列统计行 + 压缩高度 ====== */
.sis-feature-grid, .sgis-feature-grid { gap: 8px !important; margin-bottom: 8px !important; }
.sgis-feature-card { padding: 10px 10px 8px !important; }
.sgis-feature-head { gap: 8px !important; margin-bottom: 8px !important; }
.sgis-feature-icon { width: 30px !important; height: 30px !important; border-radius: 8px !important; }
.sgis-feature-icon svg { width: 15px !important; height: 15px !important; }
.sgis-feature-title { font-size: 13px !important; }
.sgis-feature-sub { font-size: 9px !important; margin-top: 1px !important; }
.sgis-feature-arrow { width: 14px !important; height: 14px !important; }
/* 3列统计行 — 战绩总览 */
.sgis-feature-stats-3 { grid-template-columns: repeat(3, 1fr) !important; gap: 4px !important; }
.sgis-feature-stats-3 .sgis-feature-stat { padding: 5px 4px !important; }
.sgis-feature-stats-3 .sgis-feature-stat-lbl { font-size: 8px !important; margin-bottom: 1px !important; }
.sgis-feature-stats-3 .sgis-feature-stat-val { font-size: 13px !important; }
/* 游戏人生卡片紧凑 */
.sgis-feature-card.alt .sgis-feature-stats { gap: 4px !important; }
.sgis-feature-card.alt .sgis-feature-stat { padding: 5px 4px !important; }
.sgis-feature-card.alt .sgis-feature-stat-lbl { font-size: 8px !important; margin-bottom: 1px !important; }
.sgis-feature-card.alt .sgis-feature-stat-val { font-size: 13px !important; }
.sgis-feature-extra { margin-top: 6px !important; padding: 5px 8px !important; font-size: 9px !important; }
.sgis-feature-extra svg { width: 11px !important; height: 11px !important; }
/* ====== 41. SGIS XP 卡片 — 保持金色进度条(原版设计),恢复中性背景 ====== */
.sgis-xp-card {
border: 1px solid rgba(255, 255, 255, 0.06) !important;
background: linear-gradient(160deg, rgba(255, 255, 255, 0.035) 0%, rgba(15, 17, 35, 0.5) 100%) !important;
}
/* XP 进度条和图标保持原版金色系,不做覆盖 */
/* XP 等级 next → 恢复紫色 */
.sgis-xp-levels .next { color: #c4b5fd !important; }
/* XP 累计数值 → 恢复紫色 */
.sgis-xp-total b { color: #c4b5fd !important; }
/* ====== 42. SGIS 社交标题 ====== */
.sgis-section-title.sgis-social-title {
border-bottom-color: rgba(139, 92, 246, 0.08) !important;
}
/* ====== 43. SGIS copy 按钮 hover ====== */
.sgis-copy-btn:hover {
background: rgba(255, 255, 255, 0.1) !important;
color: #fff !important;
}`;
// v2.11.40: 经典蓝主题层——由 scripts/gen-sglv-theme-blue.js 生成(典雅紫层色值镜像映射
// + 主脚本 CSS 常量/共享库中原生紫色规则的换色覆盖),请勿手改,重新运行生成器即可再生。
const CSS_THEME_BLUE =`
/* =====================================================================
* v2.11.40 经典蓝主题层 —— scripts/gen-sglv-theme-blue.js 自动生成,勿手改(重跑再生)
* Part A: 典雅紫主题层(CSS_THEME_PURPLE)色值镜像映射 —— 覆盖面板/侧边栏全部可见表面
* Part B: 主脚本 CSS 常量原生紫色规则 → 蓝色瘦身覆盖(仅换色声明 + !important)
* Part C: 共享样式库(sglv-shared-css)原生紫色规则 → 蓝色瘦身覆盖
* Part D: 侧边栏手工精调(靛蓝暗基底→Steam 深蓝/标题栏柔化,自动映射盲区)
* Part E: 收藏库(sglv-game-collection)面板 CSS 紫色覆盖(html[data-sglv-theme] 前缀,
* 特异性压过其懒注入且带 220 处 !important 的