// ==UserScript== // @name Steam 好友动态 - 现代化增强版 // @namespace http://tampermonkey.net/ // @version 4.5.1 // @description 保留原始页面性能,添加精致分类筛选、懒加载、美化好友浮窗(显示全部好友、等级/国旗/封禁/游戏数)、个人面板Hero组件完善、独立设置窗口、个人游戏库浮窗(游戏库标签页)、好友动态弹窗(游戏库/时长/最近)、图片浮层优化、全屏瀑布流模式、精美浮动按钮;v4.5.0 修复绝版数据展示(成就-1/0误显、卡片无封面、列表封面display:none错位),列表视图每页增至30,引用lib core多CDN封面兜底+filters=basic+占位div+GetItems批量加载,精简浮动按钮移除独立设置入口,打开游戏库/好友动态未设API时先弹设置浮窗。v4.5.1 绝版数据源切换至xi-xu.me,新增年度大作标签页(2020-2026大作按年度展示+已拥有筛选+封面加载)。 // @author SmallFork & SmallRob // @match https://steamcommunity.com/id/*/home/ // @match https://steamcommunity.com/id/*/home // @match https://steamcommunity.com/profiles/*/home/ // @match https://steamcommunity.com/profiles/*/home // @match https://steamcommunity.com/sharedfiles/filedetails* // @match https://steamcommunity.com/sharedfiles/filedetails/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @resource delistedData https://xget.xi-xu.me/gh/SmallRob/steam-namespace/raw/refs/heads/main/data/steam_delisted_apps.json // @resource annualAaaData https://xget.xi-xu.me/gh/SmallRob/steam-namespace/raw/refs/heads/main/data/2026.json // @connect store.steampowered.com // @connect aiseartifacts.blob.core.windows.net // @connect api.steampowered.com // @connect xget.xi-xu.me // @run-at document-start // @license MIT // ==/UserScript== (function() { 'use strict'; // ==================== CSS变量 ==================== const CSS_VARS = ':root{--sf-bg:#1b2838;--sf-bg2:#171a21;--sf-card:rgba(23,26,33,.8);--sf-border:rgba(255,255,255,.06);--sf-hover:rgba(255,255,255,.08);--sf-text:#c7d5e0;--sf-dim:#8f98a0;--sf-accent:#66c0f4;--sf-green:#4caf50;--sf-orange:#ff9800;--sf-radius:12px;--sf-shadow:0 2px 12px rgba(0,0,0,.2);--sf-transition:all .3s ease;--sf-purple:#8b5cf6;--sf-rose:#f43f5e;--sf-amber:#f59e0b;--sf-blue:#3b82f6;--sf-blue-strong:#2563eb;--sf-fp-bg-card:rgba(30,41,59,0.95);--sf-fp-border:rgba(102,192,244,0.2);--sf-fp-border-focus:rgba(102,192,244,0.5);--sf-fp-text-primary:#f8fafc;--sf-fp-text-secondary:#94a3b8;--sf-fp-text-dim:#64748b;--sf-fp-blue:#66c0f4;--sf-fp-blue-strong:#3b82f6;--sf-fp-green:#10b981;--sf-fp-green-text:#34d399;--sf-fp-purple:#8b5cf6;--sf-fp-purple-text:#c084fc;--sf-fp-amber:#f59e0b;--sf-fp-amber-text:#fbbf24;--sf-fp-rose:#f43f5e;--sf-fp-rose-text:#f87171;--sf-fp-radius:14px;--sf-fp-shadow:0 10px 40px rgba(0,0,0,0.5);--sf-fp-transition:all 0.25s cubic-bezier(0.4,0,0.2,1);--sf-popup-w:420px;--sf-lib-w:780px;--sf-lib-h:80vh}'; // ==================== 主布局 ==================== const CSS_LAYOUT = '.pagecontent,.bluebg{background:linear-gradient(180deg,var(--sf-bg) 0%,var(--sf-bg2) 100%)!important}#blotter_page{display:block!important;padding:0!important;max-width:940px!important;margin:0 auto!important}.blotter_content,#blotter_content{float:none!important;width:auto!important;min-width:0!important;max-width:none!important;padding:0 16px!important;box-sizing:border-box!important}#friendactivity_right_column{display:none!important}.blotter_block,.blotter_status_entry,.blotter_eventsblock,.friend_activity_header{margin-left:0!important;margin-right:0!important}.blotter_daily_rollup_line>span{max-width:none!important}'; // ==================== 卡片样式 ==================== const CSS_CARDS = '.blotter_page_title h1{font-size:28px!important;font-weight:700!important;color:#fff!important;margin-bottom:20px!important}.friend_activity_header{padding:20px!important;background:rgba(23,26,33,.6)!important;border-radius:var(--sf-radius)!important;margin-bottom:16px!important}.blotter_block{background:var(--sf-card)!important;border-radius:var(--sf-radius)!important;border:1px solid var(--sf-border)!important;margin-bottom:16px!important;overflow:hidden!important;box-shadow:var(--sf-shadow)!important;transition:var(--sf-transition)!important}.blotter_block:hover{transform:translateY(-4px)!important;box-shadow:0 8px 24px rgba(0,0,0,.4)!important;border-color:rgba(102,192,244,.2)!important}.blotter_block.sf-hidden,.blotter_block.sf-lazy-hidden{display:none!important}.blotter_author_block{padding:14px 16px!important;background:rgba(0,0,0,.2)!important}.blotter_avatar_holder .playerAvatar{border-radius:8px!important;box-shadow:0 2px 8px rgba(0,0,0,.3)!important}.blotter_avatar_holder .playerAvatar img{border-radius:8px!important}.blotter_author_block a[data-miniprofile]{font-size:14px!important;font-weight:600!important;color:var(--sf-accent)!important;cursor:pointer!important}.blotter_author_block a[data-miniprofile]:hover{color:#fff!important}.blotter_author_block a[data-miniprofile]:focus-visible{outline:2px solid var(--sf-accent)!important;outline-offset:2px!important;border-radius:3px!important}'; // ==================== 截图样式 ==================== const CSS_SCREENSHOTS = '.blotter_screenshot{border-radius:0!important;border:none!important;overflow:visible!important}.blotter_screenshot_image{border-radius:0!important;overflow:hidden!important}.blotter_screenshot_image img{transition:transform .3s ease!important}.blotter_block:hover .blotter_screenshot_image img{transform:scale(1.03)!important}.blotter_screenshot_title{padding:12px 16px!important;font-size:13px!important;color:var(--sf-text)!important;line-height:1.5!important;background:rgba(0,0,0,.1)!important}.blotter_screenshot_gallery_image{border-radius:8px!important;margin:0!important;padding:0!important;display:inline-block!important;flex-shrink:0!important;height:72px!important;width:auto!important;overflow:hidden!important;cursor:pointer!important}.blotter_screenshot_gallery_image img{height:72px!important;width:auto!important;max-width:none!important;object-fit:cover!important;display:block!important;border-radius:6px!important}.highlight_strip{position:relative!important;width:100%!important;min-height:84px!important;height:auto!important;padding:6px 0!important;background:rgba(0,0,0,.35)!important;display:block!important;clear:both!important;overflow:visible!important;border-top:1px solid rgba(255,255,255,.05)!important}.highlight_strip_scroll{position:relative!important;width:100%!important;height:auto!important;min-height:72px!important;display:flex!important;flex-wrap:nowrap!important;overflow-x:auto!important;overflow-y:hidden!important;gap:4px!important;padding:0 8px!important;align-items:center!important}.highlight_strip_scroll::-webkit-scrollbar{height:6px}.highlight_strip_scroll::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}.highlight_strip_scroll::-webkit-scrollbar-track{background:transparent}.blotter_screenshot_fullscreen{background:rgba(0,0,0,.95)!important}.blotter_screenshot_fullscreen img{border-radius:8px!important;max-width:95vw!important;max-height:90vh!important}'; // ==================== 游戏样式 ==================== const CSS_GAMES = '.blotter_userstatus_game{height:auto!important}.blotter_userstatus_game img{height:auto!important}.blotter_gamepurchase{border:none!important}.blotter_gamepurchase_content{padding:16px!important;display:flex!important;flex-wrap:wrap!important;gap:8px!important}.blotter_gamepurchase_logo{border-radius:8px!important;overflow:hidden!important;transition:transform .2s ease!important;box-shadow:0 2px 8px rgba(0,0,0,.3)!important}.blotter_gamepurchase_logo:hover{transform:scale(1.05)!important}.blotter_gamepurchase_logo img{border-radius:8px!important}.blotter_gamepurchase_logo.single_item{width:200px!important}.plus_block{background:rgba(255,255,255,.05)!important;border-radius:8px!important;color:var(--sf-dim)!important;font-weight:600!important}.blotter_gamepurchase_details{padding:12px 16px!important;font-size:12px!important;color:var(--sf-dim)!important}.blotter_recommendation{padding:16px!important}.blotter_recommendation_game{display:flex!important;align-items:center!important;gap:12px!important;margin-bottom:12px!important}.blotter_recommendation_game img{border-radius:8px!important}.blotter_recommendation_game_name{font-size:15px!important;font-weight:600!important;color:#fff!important}.blotter_recommendation_rating{display:flex!important;align-items:center!important;gap:8px!important;margin-bottom:10px!important}.blotter_recommendation_rating img{width:24px!important;height:24px!important}.blotter_recommendation_desc{font-size:13px!important;color:var(--sf-text)!important;line-height:1.6!important;padding:12px!important;background:rgba(0,0,0,.15)!important;border-radius:8px!important}'; // ==================== 工坊 ==================== const CSS_WORKSHOP = '.blotter_workshopitempublished{border:none!important}.blotter_workshopitem{padding:16px!important}.dynamiclink_box{border-radius:8px!important;overflow:hidden!important;background:rgba(0,0,0,.3)!important;border:1px solid var(--sf-border)!important;transition:all .2s ease!important}.dynamiclink_box:hover{border-color:rgba(102,192,244,.3)!important}.dynamiclink_preview{border-radius:8px 8px 0 0!important}.dynamiclink_content{padding:12px!important}.dynamiclink_name{font-size:14px!important;font-weight:600!important;color:#fff!important}.dynamiclink_type{font-size:12px!important;color:var(--sf-dim)!important}.dynamiclink_author{font-size:12px!important;color:var(--sf-text)!important}.dynamiclink_authorname{color:var(--sf-accent)!important}.dynamiclink_description{font-size:12px!important;color:var(--sf-dim)!important;margin-top:8px!important}'; // ==================== 控件 ==================== const CSS_CONTROLS = '.blotter_control_container{padding:12px 16px!important;background:rgba(0,0,0,.15)!important;border-top:1px solid rgba(255,255,255,.05)!important}.btn_grey_grey,.btn_grey_white_innerfade{background:rgba(255,255,255,.08)!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:6px!important;color:var(--sf-text)!important;transition:all .2s ease!important}.btn_grey_grey:hover,.btn_grey_white_innerfade:hover{background:rgba(255,255,255,.15)!important;color:#fff!important;border-color:rgba(255,255,255,.2)!important}.ico16.thumb_up{filter:brightness(1.2)!important}.blotter_voters_names{font-size:12px!important;color:var(--sf-dim)!important}.blotter_voters_names a{color:var(--sf-accent)!important}.blotter_comment_thread{background:rgba(0,0,0,.1)!important;border-radius:8px!important;margin:12px 16px!important}.comment_footer_ctn{display:none!important}.commentthread_area{background:transparent!important;border:none!important}.commentthread_textarea{background:rgba(0,0,0,.4)!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:8px!important;color:#fff!important;font-size:13px!important;padding:10px 12px!important;height:60px!important;min-height:60px!important;max-height:150px!important;resize:vertical!important;box-sizing:border-box!important}.commentthread_textarea:focus{border-color:rgba(102,192,244,.5)!important;box-shadow:inset 0 0 0 1px rgba(102,192,244,.3)!important}.btn_green_white_innerfade{background:linear-gradient(135deg,#4caf50 0%,#3d8b40 100%)!important;border-radius:6px!important;font-weight:500!important}'; // ==================== 状态日期 ==================== const CSS_DATE_STATUS = '.blotter_day{margin-top:20px!important}.blotter_day_header{display:flex!important;align-items:center!important;padding:10px 16px!important;background:rgba(0,0,0,.3)!important;border-radius:8px!important;margin-bottom:12px!important}.blotter_day_header h2{font-size:14px!important;font-weight:600!important;color:var(--sf-dim)!important}.blotter_status_entry{background:var(--sf-card)!important;border-radius:var(--sf-radius)!important;border:1px solid var(--sf-border)!important;padding:12px!important;margin-bottom:16px!important;box-sizing:border-box!important}#blotter_statuspost_textarea{background:rgba(0,0,0,.4)!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:8px!important;color:#fff!important;font-size:14px!important;padding:12px!important;height:80px!important;min-height:80px!important;max-height:200px!important;resize:vertical!important;box-sizing:border-box!important}#blotter_statuspost_textarea:focus{border-color:rgba(102,192,244,.5)!important;box-shadow:inset 0 0 0 1px rgba(102,192,244,.3)!important}.blotter_appselect{background:rgba(255,255,255,.05)!important;border-radius:6px!important;border:1px solid rgba(255,255,255,.1)!important}.blotter_status_submit_ctn{display:flex!important;align-items:center!important;gap:10px!important;justify-content:flex-end!important}.blotter_eventsblock{background:rgba(23,26,33,.6)!important;border-radius:var(--sf-radius)!important;border:1px solid var(--sf-border)!important;padding:16px!important;margin-bottom:16px!important}.blotter_eventsblock_title{font-size:14px!important;font-weight:600!important;color:#fff!important;margin-bottom:12px!important}'; // ==================== 模态 ==================== const CSS_MODAL = '#modalContent{background:var(--sf-card)!important;border-radius:var(--sf-radius)!important;border:1px solid var(--sf-border)!important;box-shadow:0 8px 40px rgba(0,0,0,.5)!important;overflow:hidden!important}#modalContentTitleBar{background:linear-gradient(135deg,var(--sf-bg) 0%,#2a475e 100%)!important;border-bottom:1px solid var(--sf-border)!important;padding:10px 16px!important;display:flex!important;align-items:center!important;gap:10px!important}#modalContentTitleBarLink{color:var(--sf-accent)!important;font-size:13px!important;font-weight:500!important;text-decoration:none!important;flex:1!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}#modalContentTitleBarLink:hover{color:#fff!important}#modalContentTitleBarImageLink img{opacity:.5!important;transition:opacity .2s!important}#modalContentTitleBarImageLink:hover img{opacity:1!important}#modalContentDismiss{width:28px!important;height:28px!important;border-radius:50%!important;background:rgba(255,255,255,.1)!important;border:none!important;cursor:pointer!important;display:flex!important;align-items:center!important;justify-content:center!important;transition:all .2s!important;padding:0!important}#modalContentDismiss:hover{background:rgba(255,255,255,.2)!important}#modalContentDismiss img{display:none!important}#modalContentDismiss::after{content:\'✕\';color:var(--sf-text)!important;font-size:14px!important}#modalContentFrameContainer{background:rgba(0,0,0,.2)!important}.modalContent_iFrame{border:none!important;border-radius:0!important}'; // ==================== 筛选栏 ==================== const CSS_FILTER = '#sf-filter-bar{position:sticky!important;top:0!important;z-index:1000!important;background:rgba(23,26,33,.95)!important;backdrop-filter:blur(10px)!important;-webkit-backdrop-filter:blur(10px)!important;padding:10px 16px!important;border-radius:var(--sf-radius)!important;margin-bottom:16px!important;box-shadow:0 4px 20px rgba(0,0,0,.3)!important}.sf-filter-row{display:flex!important;align-items:center!important;gap:6px!important;overflow:hidden!important;transition:max-height .35s cubic-bezier(.4,0,.2,1)!important}.sf-filter-row.sf-compact{max-height:42px!important;flex-wrap:nowrap!important}.sf-filter-row.sf-expanded{max-height:200px!important;flex-wrap:wrap!important}.sf-filter-btn{display:inline-flex!important;align-items:center!important;gap:5px!important;padding:6px 12px!important;border-radius:8px!important;background:rgba(255,255,255,.05)!important;border:1px solid transparent!important;color:var(--sf-text)!important;font-size:13px!important;font-weight:500!important;cursor:pointer!important;transition:all .2s!important;white-space:nowrap!important;flex-shrink:0!important}.sf-filter-btn:hover{background:var(--sf-hover)!important;color:#fff!important}.sf-filter-btn.active{background:linear-gradient(135deg,var(--sf-accent) 0%,#4a9bc7 100%)!important;color:#fff!important;border-color:rgba(102,192,244,.3)!important;box-shadow:0 2px 10px rgba(102,192,244,.3)!important}.sf-filter-icon{width:15px!important;height:15px!important;fill:currentColor!important;flex-shrink:0!important}.sf-filter-count{margin-left:3px!important;padding:1px 5px!important;background:rgba(255,255,255,.15)!important;border-radius:10px!important;font-size:11px!important;min-width:18px!important;text-align:center!important}.sf-filter-btn.active .sf-filter-count{background:rgba(255,255,255,.25)!important}.sf-filter-more{display:inline-flex!important;align-items:center!important;gap:4px!important;padding:6px 12px!important;border-radius:8px!important;background:rgba(255,255,255,.04)!important;border:1px solid var(--sf-border)!important;color:var(--sf-text)!important;font-size:13px!important;font-weight:500!important;cursor:pointer!important;transition:all .2s!important;white-space:nowrap!important;flex-shrink:0!important}.sf-filter-more:hover{background:var(--sf-hover)!important;color:#fff!important}.sf-filter-more svg{width:14px!important;height:14px!important;fill:currentColor!important;transition:transform .25s ease!important}.sf-filter-more.sf-open svg{transform:rotate(180deg)!important}'; // ==================== 加载/空状态 ==================== const CSS_LOAD_EMPTY = '#sf-load-more{display:flex!important;justify-content:center!important;padding:20px!important}.sf-load-btn{padding:12px 32px!important;border-radius:8px!important;background:linear-gradient(135deg,var(--sf-accent) 0%,#4a9bc7 100%)!important;color:#fff!important;font-size:14px!important;font-weight:600!important;cursor:pointer!important;transition:all .2s!important;border:none!important}.sf-load-btn:hover{transform:scale(1.05)!important;box-shadow:0 4px 15px rgba(102,192,244,.4)!important}.sf-load-btn:disabled{opacity:.6!important;cursor:not-allowed!important;transform:none!important}.sf-empty-state{text-align:center!important;padding:60px 20px!important;color:var(--sf-dim)!important}.sf-empty-icon{font-size:48px!important;margin-bottom:16px!important}.sf-empty-text{font-size:16px!important}'; // ==================== FAB 按钮(精美 SVG 图标 + 玻璃拟态)==================== const CSS_FAB_BASE = '#sf-waterfall-btn,#sf-friend-btn,#sf-library-btn,#sf-top-btn,#sf-layout-btn{position:fixed!important;right:30px!important;width:50px!important;height:50px!important;border-radius:50%!important;border:none!important;cursor:pointer!important;z-index:9999!important;background:rgba(30,41,59,0.85)!important;backdrop-filter:blur(12px)!important;-webkit-backdrop-filter:blur(12px)!important;box-shadow:0 4px 20px rgba(0,0,0,.35),inset 0 1px 0 rgba(255,255,255,.08)!important;transition:all .25s cubic-bezier(.34,1.56,.64,1)!important;display:flex!important;align-items:center!important;justify-content:center!important;color:#f8fafc!important}#sf-waterfall-btn svg,#sf-friend-btn svg,#sf-library-btn svg,#sf-top-btn svg,#sf-layout-btn svg{width:24px!important;height:24px!important;fill:none!important;stroke:currentColor!important;stroke-width:2!important;stroke-linecap:round!important;stroke-linejoin:round!important;transition:all .25s ease!important}'; const CSS_FAB_EACH = '#sf-waterfall-btn{bottom:30px!important;background:linear-gradient(135deg,rgba(139,92,246,0.95) 0%,rgba(124,58,237,0.95) 100%)!important}#sf-friend-btn{bottom:90px!important;background:linear-gradient(135deg,rgba(59,130,246,0.95) 0%,rgba(37,99,235,0.95) 100%)!important}#sf-library-btn{bottom:150px!important;background:linear-gradient(135deg,rgba(139,92,246,0.95) 0%,rgba(109,40,217,0.95) 100%)!important}#sf-layout-btn{bottom:210px!important;background:linear-gradient(135deg,rgba(38,166,154,0.95) 0%,rgba(0,121,107,0.95) 100%)!important}#sf-top-btn{bottom:270px!important;background:linear-gradient(135deg,rgba(84,110,122,0.95) 0%,rgba(55,71,79,0.95) 100%)!important;opacity:0!important;visibility:hidden!important;transform:scale(.8)!important}'; const CSS_FAB_HOVER = '#sf-waterfall-btn:hover,#sf-friend-btn:hover,#sf-library-btn:hover,#sf-top-btn:hover,#sf-layout-btn:hover{transform:scale(1.12)!important;box-shadow:0 8px 28px rgba(0,0,0,.45),0 0 20px rgba(102,192,244,.25)!important;filter:brightness(1.1)!important}#sf-waterfall-btn:hover{box-shadow:0 8px 28px rgba(0,0,0,.45),0 0 22px rgba(139,92,246,.45)!important}#sf-friend-btn:hover{box-shadow:0 8px 28px rgba(0,0,0,.45),0 0 22px rgba(59,130,246,.45)!important}#sf-library-btn:hover{box-shadow:0 8px 28px rgba(0,0,0,.45),0 0 22px rgba(139,92,246,.45)!important}#sf-layout-btn:hover{box-shadow:0 8px 28px rgba(0,0,0,.45),0 0 22px rgba(38,166,154,.45)!important}#sf-top-btn:hover{box-shadow:0 6px 20px rgba(84,110,122,.6)!important;background:linear-gradient(135deg,rgba(96,125,139,0.95) 0%,rgba(69,90,100,0.95) 100%)!important}#sf-top-btn.sf-show{opacity:1!important;visibility:visible!important;transform:scale(1)!important}'; const CSS_FAB_LABEL = '.sf-fab-label{position:absolute;right:60px;background:rgba(23,26,33,0.9);border:1px solid rgba(255,255,255,0.1);padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;color:#fff;white-space:nowrap;opacity:0;visibility:hidden;transform:translateX(-8px);transition:all .2s ease;pointer-events:none;box-shadow:0 4px 12px rgba(0,0,0,.3)}#sf-waterfall-btn:hover .sf-fab-label,#sf-friend-btn:hover .sf-fab-label,#sf-library-btn:hover .sf-fab-label,#sf-top-btn:hover .sf-fab-label,#sf-layout-btn:hover .sf-fab-label{opacity:1;visibility:visible;transform:translateX(0)}'; // ==================== 瀑布流 ==================== const CSS_WATERFALL = '#sf-wf-overlay{position:fixed!important;inset:0!important;z-index:99990!important;background:linear-gradient(180deg,#1b2838 0%,#171a21 100%)!important;opacity:0!important;visibility:hidden!important;transition:opacity .35s ease,visibility .35s ease!important;overflow-y:auto!important;overflow-x:hidden!important}#sf-wf-overlay.sf-wf-open{opacity:1!important;visibility:visible!important}.sf-wf-header{position:sticky!important;top:0!important;z-index:99992!important;background:rgba(23,26,33,.97)!important;backdrop-filter:blur(12px)!important;-webkit-backdrop-filter:blur(12px)!important;padding:14px 24px!important;display:flex!important;align-items:center!important;justify-content:space-between!important;border-bottom:1px solid rgba(255,255,255,.08)!important;box-shadow:0 4px 20px rgba(0,0,0,.3)!important}.sf-wf-title{font-size:18px!important;font-weight:700!important;color:#fff!important;display:flex!important;align-items:center!important;gap:10px!important}.sf-wf-title svg{width:22px!important;height:22px!important;fill:#ab47bc!important}.sf-wf-count{font-size:13px!important;color:var(--sf-dim)!important;font-weight:400!important;margin-left:8px!important}.sf-wf-close{width:36px!important;height:36px!important;border-radius:50%!important;background:rgba(255,255,255,.08)!important;border:1px solid rgba(255,255,255,.1)!important;cursor:pointer!important;display:flex!important;align-items:center!important;justify-content:center!important;transition:all .2s ease!important;color:var(--sf-text)!important;font-size:18px!important}.sf-wf-close:hover{background:rgba(255,82,82,.3)!important;border-color:rgba(255,82,82,.4)!important;color:#ff5252!important}.sf-wf-close svg{width:18px!important;height:18px!important;fill:currentColor!important}.sf-wf-body{padding:20px 24px 40px!important;max-width:1600px!important;margin:0 auto!important}.sf-wf-grid{display:flex!important;gap:16px!important;align-items:flex-start!important;justify-content:center!important}.sf-wf-col{flex:1!important;min-width:0!important;max-width:380px!important;display:flex!important;flex-direction:column!important;gap:14px!important}'; const CSS_WF_OVERRIDES = '#sf-wf-overlay .blotter_block{background:var(--sf-card)!important;border-radius:var(--sf-radius)!important;border:1px solid var(--sf-border)!important;overflow:visible!important;box-shadow:var(--sf-shadow)!important;transition:var(--sf-transition)!important;margin-bottom:0!important;transform:none!important}#sf-wf-overlay .blotter_block:hover{transform:translateY(-4px)!important;box-shadow:0 8px 24px rgba(0,0,0,.4)!important;border-color:rgba(171,71,188,.25)!important}#sf-wf-overlay .blotter_block.sf-hidden{display:none!important}#sf-wf-overlay .blotter_control_container{padding:10px 14px!important}#sf-wf-overlay .blotter_author_block{padding:12px 14px!important}#sf-wf-overlay .blotter_screenshot_title{padding:10px 14px!important}#sf-wf-overlay .blotter_gamepurchase{overflow:hidden!important}#sf-wf-overlay .blotter_gamepurchase_content{padding:10px!important;display:flex!important;flex-wrap:wrap!important;gap:8px!important;justify-content:flex-start!important}#sf-wf-overlay .blotter_gamepurchase_logo{flex:1 1 calc(50% - 4px)!important;min-width:0!important;max-width:none!important;border-radius:6px!important;overflow:hidden!important}#sf-wf-overlay .blotter_gamepurchase_logo:only-of-type{flex:1 1 100%!important}#sf-wf-overlay .blotter_gamepurchase_logo img{width:100%!important;height:auto!important;display:block!important;border-radius:6px!important}#sf-wf-overlay .blotter_gamepurchase_logo img[alt*="No Image" i],#sf-wf-overlay .blotter_gamepurchase_logo img[src*="data:image/svg+xml"]{display:none!important}#sf-wf-overlay .plus_block{display:none!important}#sf-wf-overlay .blotter_gamepurchase_details{padding:8px 10px!important;font-size:11px!important;color:var(--sf-dim)!important;line-height:1.4!important;word-break:break-word!important}#sf-wf-overlay .blotter_recommendation{padding:14px!important;overflow:hidden!important;display:flex!important;flex-direction:column!important;gap:10px!important}#sf-wf-overlay .blotter_recommendation_game{flex-shrink:0!important}'; const CSS_WF_LOADING = '.sf-wf-loading{text-align:center!important;padding:40px 20px!important;color:var(--sf-dim)!important}.sf-wf-loading-spinner{width:32px!important;height:32px!important;border:3px solid rgba(171,71,188,.3)!important;border-top-color:#ab47bc!important;border-radius:50%!important;animation:sf-spin .8s linear infinite!important;margin:0 auto 12px!important}@keyframes sf-spin{to{transform:rotate(360deg)}}#sf-wf-loadmore{display:flex!important;justify-content:center!important;padding:24px!important}.sf-wf-load-btn{padding:12px 32px!important;border-radius:8px!important;background:linear-gradient(135deg,#ab47bc 0%,#7b1fa2 100%)!important;color:#fff!important;font-size:14px!important;font-weight:600!important;cursor:pointer!important;transition:all .2s!important;border:none!important}.sf-wf-load-btn:hover{transform:scale(1.05)!important;box-shadow:0 4px 15px rgba(171,71,188,.4)!important}.sf-wf-load-btn:disabled{opacity:.6!important;cursor:not-allowed!important;transform:none!important}.sf-wf-empty{text-align:center!important;padding:80px 20px!important;color:var(--sf-dim)!important}.sf-wf-empty-icon{font-size:56px!important;margin-bottom:16px!important}.sf-wf-empty-text{font-size:18px!important}'; // ==================== 好友面板 v4.0(参考 1.0.16 好友管理器)==================== const CSS_FRIEND_PANEL = '#sf-friend-panel{position:fixed!important;bottom:150px!important;right:30px!important;width:380px!important;max-height:78vh!important;background:rgba(23,26,33,0.97)!important;border:1px solid var(--sf-fp-border)!important;border-radius:var(--sf-fp-radius)!important;box-shadow:var(--sf-fp-shadow)!important;z-index:9998!important;overflow:hidden!important;opacity:0!important;visibility:hidden!important;transform:translateY(20px) scale(0.96)!important;transition:var(--sf-fp-transition)!important;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--sf-fp-text-primary);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);display:flex!important;flex-direction:column}#sf-friend-panel.open{opacity:1!important;visibility:visible!important;transform:translateY(0) scale(1)!important}.sf-fp-header{background:linear-gradient(135deg,rgba(59,130,246,0.18) 0%,rgba(15,23,42,0.7) 100%);padding:12px 16px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--sf-fp-border);flex-shrink:0;position:relative;overflow:hidden}.sf-fp-header::after{content:"";position:absolute;top:-30px;right:-30px;width:80px;height:80px;background:radial-gradient(circle,rgba(102,192,244,0.12) 0%,transparent 70%);pointer-events:none}.sf-fp-title{font-size:14px;font-weight:700;color:#fff;display:flex;align-items:center;gap:8px;position:relative;z-index:1}.sf-fp-title svg{width:18px;height:18px;stroke:var(--sf-fp-blue);stroke-width:2.2;fill:none}.sf-fp-close{width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;color:#94a3b8;position:relative;z-index:1}.sf-fp-close:hover{background:rgba(244,63,94,0.18);color:var(--sf-fp-rose-text)}.sf-fp-close svg{width:14px;height:14px;stroke:currentColor;stroke-width:2.2;fill:none}.sf-fp-content{flex:1;overflow-y:auto;padding:10px;scrollbar-width:thin;scrollbar-color:rgba(102,192,244,0.3) transparent}.sf-fp-content::-webkit-scrollbar{width:5px}.sf-fp-content::-webkit-scrollbar-thumb{background:rgba(102,192,244,0.3);border-radius:3px}.sf-fp-content::-webkit-scrollbar-track{background:transparent}.sf-fp-header-actions{display:flex;align-items:center;gap:4px;position:relative;z-index:1}.sf-fp-settings{width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;color:#94a3b8}.sf-fp-settings:hover{background:rgba(102,192,244,0.15);color:var(--sf-fp-blue)}.sf-fp-settings:disabled{opacity:.5;cursor:not-allowed}.sf-fp-settings svg{width:14px;height:14px;stroke:currentColor;stroke-width:2.2;fill:none}.sf-fp-syncing svg{animation:sf-spin 1s linear infinite}'; const CSS_FRIEND_OWN = '.sf-fp-own{background:linear-gradient(135deg,rgba(59,130,246,0.12) 0%,rgba(30,41,59,0.9) 100%);border:1px solid rgba(102,192,244,0.25);border-radius:var(--sf-fp-radius);padding:12px;display:flex;align-items:center;gap:12px;margin-bottom:10px;position:relative;overflow:hidden;transition:var(--sf-fp-transition)}.sf-fp-own:hover{border-color:rgba(102,192,244,0.4);transform:translateY(-1px)}.sf-fp-own::before{content:"";position:absolute;top:-20px;right:-20px;width:80px;height:80px;background:radial-gradient(circle,rgba(59,130,246,0.12) 0%,transparent 70%);pointer-events:none}.sf-fp-own-avatar{position:relative;width:46px;height:46px;flex-shrink:0}.sf-fp-own-avatar img{width:100%;height:100%;border-radius:50%;object-fit:cover;border:2px solid rgba(102,192,244,0.4);transition:var(--sf-fp-transition)}.sf-fp-own:hover .sf-fp-own-avatar img{border-color:var(--sf-fp-blue-strong);box-shadow:0 0 14px rgba(59,130,246,0.4)}.sf-fp-own-frame{position:absolute;inset:-6px;pointer-events:none}.sf-fp-own-frame img{width:100%;height:100%}.sf-fp-own-dot{position:absolute;bottom:0;right:0;width:11px;height:11px;border-radius:50%;border:2px solid var(--sf-fp-bg-card);background:#94a3b8}.sf-fp-own-dot.online{background:var(--sf-fp-green);box-shadow:0 0 6px rgba(16,185,129,0.5)}.sf-fp-own-dot.in-game{background:var(--sf-fp-purple);box-shadow:0 0 6px rgba(139,92,246,0.5)}.sf-fp-own-info{flex:1;min-width:0;position:relative;z-index:1}.sf-fp-own-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:3px}.sf-fp-own-name{font-size:14px;font-weight:700;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:170px;cursor:pointer;text-decoration:none}.sf-fp-own-name:hover{color:var(--sf-fp-blue);text-decoration:underline}.sf-fp-own-level{display:inline-flex;align-items:center;gap:2px;background:rgba(245,158,11,0.15);border:1px solid rgba(245,158,11,0.3);border-radius:6px;padding:1px 6px;font-size:10px;font-weight:700;color:var(--sf-fp-amber-text)}.sf-fp-own-level svg{width:10px;height:10px;fill:currentColor}.sf-fp-own-meta{display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:11px;color:var(--sf-fp-text-secondary)}.sf-fp-own-flag{width:16px;height:12px;display:inline-block;vertical-align:middle;border-radius:2px;flex-shrink:0}.sf-fp-own-stat{display:inline-flex;align-items:center;gap:3px;background:rgba(255,255,255,0.05);padding:1px 6px;border-radius:4px;font-size:10px;color:var(--sf-fp-text-secondary)}.sf-fp-own-actions{display:flex;flex-direction:column;gap:4px;position:relative;z-index:1}.sf-fp-own-act{width:28px;height:28px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);border-radius:6px;color:#94a3b8;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease}.sf-fp-own-act:hover{background:rgba(59,130,246,0.15);border-color:var(--sf-fp-blue);color:var(--sf-fp-blue)}.sf-fp-own-act svg{width:14px;height:14px;stroke:currentColor;stroke-width:2.2;fill:none}.sf-fp-own-stats{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin:4px 0 2px}.sf-fp-own-stats .sf-fp-own-stat svg{width:11px;height:11px;flex-shrink:0}.sf-fp-own-stats .sf-fp-own-stat svg path{stroke-width:2}'; // ==================== 好友列表分类 ==================== const CSS_FRIEND_CATEGORY = '.sf-fp-section-title{font-size:11px;font-weight:700;color:var(--sf-fp-text-secondary);text-transform:uppercase;letter-spacing:0.5px;padding:8px 10px 6px;display:flex;align-items:center;justify-content:space-between;border-top:1px solid rgba(255,255,255,0.04);margin-top:6px}.sf-fp-section-title .sf-fp-section-count{font-size:10px;font-weight:700;padding:1px 7px;border-radius:8px;background:rgba(255,255,255,0.06);color:var(--sf-fp-text-secondary);text-transform:none;letter-spacing:0}.sf-fp-section-title.cat-ingame{border-top-color:rgba(139,92,246,0.3);color:var(--sf-fp-purple-text)}.sf-fp-section-title.cat-ingame .sf-fp-section-count{background:rgba(139,92,246,0.2);color:var(--sf-fp-purple-text)}.sf-fp-section-title.cat-online{border-top-color:rgba(16,185,129,0.3);color:var(--sf-fp-green-text)}.sf-fp-section-title.cat-online .sf-fp-section-count{background:rgba(16,185,129,0.2);color:var(--sf-fp-green-text)}.sf-fp-section-title.cat-offline{border-top-color:rgba(255,255,255,0.06);color:var(--sf-fp-text-secondary)}.sf-fp-section-title.cat-banned{color:var(--sf-fp-rose-text)}.sf-fp-section-title.cat-banned .sf-fp-section-count{background:rgba(244,63,94,0.2);color:var(--sf-fp-rose-text)}.sf-fp-list{display:flex;flex-direction:column;gap:4px;margin-bottom:4px}.sf-fp-section-title.collapsible{cursor:pointer;user-select:none;transition:var(--sf-fp-transition)}.sf-fp-section-title.collapsible:hover{background:rgba(255,255,255,0.04)}.sf-fp-section-title.collapsible::after{content:"▾";font-size:10px;margin-left:4px;transition:transform .2s ease}.sf-fp-section-title.collapsible.collapsed::after{transform:rotate(-90deg)}.sf-fp-menu-wrap{display:flex;flex-direction:column;gap:2px}.sf-fp-menu-wrap.collapsed{display:none}'; // ==================== 好友项(v4.0 强化版)==================== const CSS_FRIEND_ITEM = '.sf-fp-item{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:10px;transition:var(--sf-fp-transition);text-decoration:none!important;color:inherit!important;cursor:pointer;position:relative}.sf-fp-item:hover{background:rgba(255,255,255,0.05);border-color:rgba(102,192,244,0.2);transform:translateX(2px)}.sf-fp-avatar{position:relative;width:36px;height:36px;flex-shrink:0}.sf-fp-avatar img{width:100%;height:100%;border-radius:50%;object-fit:cover;border:2px solid #3d4450;transition:var(--sf-fp-transition)}.sf-fp-avatar.in-game img{border-color:var(--sf-fp-purple)}.sf-fp-avatar.online img{border-color:var(--sf-fp-green)}.sf-fp-avatar.banned img{border-color:var(--sf-fp-rose-text)}.sf-fp-dot{position:absolute;bottom:-2px;right:-2px;width:11px;height:11px;border-radius:50%;border:2px solid rgba(23,26,33,0.95);background:#94a3b8}.sf-fp-dot.online{background:var(--sf-fp-green);box-shadow:0 0 6px rgba(16,185,129,0.5)}.sf-fp-dot.in-game{background:var(--sf-fp-purple);box-shadow:0 0 6px rgba(139,92,246,0.5)}.sf-fp-dot.offline{background:var(--sf-fp-text-secondary)}.sf-fp-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px;overflow:hidden}.sf-fp-name-row{display:flex;align-items:center;gap:5px;min-width:0}.sf-fp-name{font-size:13px;font-weight:600;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.sf-fp-level,.sf-fp-level-tag{font-size:10px;font-weight:700;color:var(--sf-fp-amber-text);background:rgba(245,158,11,0.12);border-radius:4px;padding:1px 4px;flex-shrink:0}.sf-fp-level-tag{display:none}.sf-fp-level-tag.sf-fp-level-show{display:inline-flex}.sf-fp-flag{width:14px;height:10px;border-radius:1px;flex-shrink:0}.sf-fp-shield{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px}.sf-fp-shield svg{width:14px;height:14px;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.sf-fp-shield.clean svg{stroke:var(--sf-fp-green)}.sf-fp-shield.banned svg{stroke:var(--sf-fp-rose-text)}.sf-fp-shield.devban svg{stroke:var(--sf-fp-amber-text)}.sf-fp-shield.banned-both svg{stroke:var(--sf-fp-rose-text)}.sf-fp-tags{display:flex;align-items:center;gap:4px;flex-wrap:nowrap;font-size:10px;color:var(--sf-fp-text-secondary);min-width:0;overflow:hidden}.sf-fp-tag{display:inline-flex;align-items:center;gap:2px;background:rgba(255,255,255,0.04);padding:1px 5px;border-radius:4px;white-space:nowrap}.sf-fp-tag.in-game{background:rgba(139,92,246,0.15);color:var(--sf-fp-purple-text)}.sf-fp-tag.online{background:rgba(16,185,129,0.12);color:var(--sf-fp-green-text)}.sf-fp-tag.games{background:rgba(59,130,246,0.12);color:#60a5fa}.sf-fp-tag.new-friend{background:rgba(244,63,94,0.12);color:var(--sf-fp-rose-text)}.sf-fp-action-btn{width:26px;height:26px;border-radius:6px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;flex-shrink:0}.sf-fp-action-btn:hover{background:rgba(139,92,246,0.15);border-color:var(--sf-fp-purple);color:var(--sf-fp-purple)}.sf-fp-action-btn svg{width:13px;height:13px;stroke:currentColor;stroke-width:2.2;fill:none}.sf-fp-item-wrap{display:flex;align-items:center;gap:5px;margin-bottom:4px}.sf-fp-item-wrap .sf-fp-item{flex:1;min-width:0}.sf-fp-level-tag{display:inline-flex;align-items:center;gap:2px}.sf-fp-level-tag svg{width:10px;height:10px;fill:currentColor;flex-shrink:0}'; // ==================== 好友面板杂项 ==================== const CSS_FRIEND_MISC = '.sf-fp-stats{background:linear-gradient(135deg,rgba(59,130,246,0.08) 0%,rgba(15,23,42,0.4) 100%);border:1px solid rgba(102,192,244,0.15);border-radius:10px;padding:8px 12px;font-size:12px;color:var(--sf-fp-text-primary);margin-bottom:10px;font-weight:600;display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}.sf-fp-stats-left{display:flex;align-items:center;gap:6px}.sf-fp-stats-right{display:flex;align-items:center;gap:4px}.sf-fp-stat-pill{font-size:10px;font-weight:700;padding:2px 6px;border-radius:6px;background:rgba(255,255,255,0.05)}.sf-fp-stat-pill.games{background:rgba(59,130,246,0.15);color:#60a5fa}.sf-fp-stat-pill.banned{background:rgba(244,63,94,0.15);color:var(--sf-fp-rose-text)}.sf-fp-stats svg{width:14px;height:14px;fill:none;stroke:var(--sf-fp-blue);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.sf-fp-action{display:flex;align-items:center;justify-content:center;gap:6px;padding:9px;margin-top:6px;border-radius:10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06);color:var(--sf-fp-text-primary);font-size:12px;font-weight:600;cursor:pointer;transition:var(--sf-fp-transition);text-decoration:none!important}.sf-fp-action:hover{background:rgba(59,130,246,0.12);border-color:rgba(59,130,246,0.3);color:var(--sf-fp-blue);transform:translateY(-1px)}.sf-fp-action svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.sf-fp-action.primary{background:linear-gradient(135deg,var(--sf-fp-blue) 0%,var(--sf-fp-blue-strong) 100%);color:#fff;border-color:transparent}.sf-fp-action.primary:hover{filter:brightness(1.15);transform:translateY(-1px);color:#fff}.sf-fp-action.purple{background:linear-gradient(135deg,var(--sf-fp-purple) 0%,#6d28d9 100%);color:#fff;border-color:transparent}.sf-fp-action.purple:hover{filter:brightness(1.15);transform:translateY(-1px);color:#fff}.sf-fp-menu-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:8px;background:rgba(255,255,255,0.03);color:var(--sf-fp-text-primary);font-size:12px;cursor:pointer;transition:var(--sf-fp-transition);margin-bottom:4px;text-decoration:none!important}.sf-fp-menu-item:hover{background:rgba(102,192,244,0.1);color:var(--sf-fp-blue);transform:translateX(2px)}.sf-fp-menu-item svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.sf-fp-empty{text-align:center;padding:24px 20px;color:var(--sf-fp-text-secondary);font-size:12px;background:rgba(255,255,255,0.02);border-radius:10px;border:1px dashed rgba(255,255,255,0.06)}.sf-fp-empty svg{width:32px;height:32px;fill:none;stroke:currentColor;stroke-width:1.5;opacity:0.5;margin-bottom:8px}.sf-fp-toast{position:fixed;bottom:30px;left:50%;transform:translateX(-50%) translateY(20px);background:rgba(23,26,33,0.97);color:#fff;padding:10px 18px;border-radius:10px;font-size:13px;font-weight:500;z-index:100002;opacity:0;pointer-events:none;transition:all .3s ease;border:1px solid rgba(102,192,244,0.3);box-shadow:0 4px 20px rgba(0,0,0,0.4)}.sf-fp-toast.sfp-show{opacity:1;transform:translateX(-50%) translateY(0)}'; const CSS_FRIEND_FOOTER = '.sf-fp-footer{padding:8px 14px;background:rgba(15,23,42,0.6);border-top:1px solid var(--sf-fp-border);font-size:11px;color:var(--sf-fp-text-secondary);display:flex;justify-content:space-between;align-items:center;flex-shrink:0;gap:6px}.sf-fp-footer .sf-fp-footer-tip{display:flex;align-items:center;gap:5px}.sf-fp-footer .sf-fp-footer-tip svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;color:var(--sf-fp-blue)}.sf-fp-footer .sf-fp-footer-key{font-size:10px;background:rgba(255,255,255,0.05);padding:2px 5px;border-radius:4px;cursor:pointer;transition:all .2s ease}.sf-fp-footer .sf-fp-footer-key:hover{background:rgba(59,130,246,0.15);color:var(--sf-fp-blue)}'; // ==================== 好友动态浮窗(右侧滑出)==================== const CSS_FA_PANEL = '#sf-fa-panel{position:fixed!important;top:0!important;right:0!important;width:var(--sf-popup-w)!important;max-width:96vw!important;height:100vh!important;background:rgba(17,24,39,0.98)!important;border-left:1px solid var(--sf-fp-border)!important;box-shadow:-12px 0 50px rgba(0,0,0,0.6)!important;z-index:10000!important;display:flex!important;flex-direction:column;transform:translateX(100%)!important;transition:transform .35s cubic-bezier(0.4,0,0.2,1)!important;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--sf-fp-text-primary);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}#sf-fa-panel.sfp-open{transform:translateX(0)!important}.sf-fa-header{background:linear-gradient(135deg,rgba(139,92,246,0.18) 0%,rgba(15,23,42,0.85) 100%);padding:12px 16px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--sf-fp-border);flex-shrink:0;position:relative;overflow:hidden}.sf-fa-header::after{content:"";position:absolute;top:-30px;right:-30px;width:100px;height:100px;background:radial-gradient(circle,rgba(139,92,246,0.15) 0%,transparent 70%);pointer-events:none}.sf-fa-title{font-size:14px;font-weight:700;color:#fff;display:flex;align-items:center;gap:8px;position:relative;z-index:1;min-width:0;flex:1;overflow:hidden}.sf-fa-title .sf-fa-avatar{width:24px;height:24px;border-radius:50%;object-fit:cover;border:1.5px solid rgba(139,92,246,0.4);flex-shrink:0}.sf-fa-title .sf-fa-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-fa-close{width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;color:#94a3b8;position:relative;z-index:1;flex-shrink:0}.sf-fa-close:hover{background:rgba(244,63,94,0.18);color:var(--sf-fp-rose-text)}.sf-fa-close svg{width:14px;height:14px;stroke:currentColor;stroke-width:2.2;fill:none}.sf-fa-tabs{display:flex;align-items:center;gap:2px;padding:6px 8px;background:rgba(15,23,42,0.5);border-bottom:1px solid var(--sf-fp-border);flex-shrink:0}.sf-fa-tab{flex:1;display:flex;align-items:center;justify-content:center;gap:5px;padding:7px 8px;border-radius:6px;background:transparent;border:none;color:var(--sf-fp-text-secondary);font-size:11px;font-weight:600;cursor:pointer;transition:all .2s ease;white-space:nowrap}.sf-fa-tab:hover{background:rgba(255,255,255,0.05);color:#fff}.sf-fa-tab.active{background:linear-gradient(135deg,rgba(139,92,246,0.25) 0%,rgba(139,92,246,0.1) 100%);color:var(--sf-fp-purple-text)}.sf-fa-tab svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;flex-shrink:0}.sf-fa-body{flex:1;overflow-y:auto;padding:12px;scrollbar-width:thin;scrollbar-color:rgba(139,92,246,0.3) transparent}.sf-fa-body::-webkit-scrollbar{width:5px}.sf-fa-body::-webkit-scrollbar-thumb{background:rgba(139,92,246,0.3);border-radius:3px}.sf-fa-body::-webkit-scrollbar-track{background:transparent}.sf-fa-loading{text-align:center;padding:40px 16px;color:var(--sf-fp-text-secondary);font-size:12px}.sf-fa-loading-spinner{width:28px;height:28px;border:3px solid rgba(139,92,246,0.2);border-top-color:var(--sf-fp-purple);border-radius:50%;animation:sf-spin .8s linear infinite;margin:0 auto 10px}'; // ==================== FA 概览卡片 ==================== const CSS_FA_OVERVIEW = '.sf-fa-hero{background:linear-gradient(135deg,rgba(59,130,246,0.12) 0%,rgba(15,23,42,0.7) 100%);border:1px solid rgba(102,192,244,0.25);border-radius:var(--sf-fp-radius);padding:14px;display:flex;align-items:center;gap:12px;margin-bottom:12px;position:relative;overflow:hidden}.sf-fa-hero::before{content:"";position:absolute;top:-30px;right:-30px;width:100px;height:100px;background:radial-gradient(circle,rgba(59,130,246,0.15) 0%,transparent 70%);pointer-events:none}.sf-fa-hero-avatar{position:relative;width:64px;height:64px;flex-shrink:0}.sf-fa-hero-avatar img{width:100%;height:100%;border-radius:50%;object-fit:cover;border:2.5px solid rgba(102,192,244,0.4);box-shadow:0 4px 14px rgba(59,130,246,0.2)}.sf-fa-hero-info{flex:1;min-width:0;position:relative;z-index:1}.sf-fa-hero-name{font-size:15px;font-weight:800;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:4px}.sf-fa-hero-meta{display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:11px;color:var(--sf-fp-text-secondary)}.sf-fa-hero-meta .sf-fp-tag{margin:0}.sf-fa-stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-bottom:12px}.sf-fa-stat{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:8px;padding:8px 10px;text-align:center;transition:var(--sf-fp-transition)}.sf-fa-stat:hover{background:rgba(255,255,255,0.06);transform:translateY(-1px)}.sf-fa-stat-val{font-size:16px;font-weight:700;color:#fff;line-height:1.2}.sf-fa-stat-lbl{font-size:10px;color:var(--sf-fp-text-secondary);margin-top:2px}.sf-fa-stat.games .sf-fa-stat-val{color:#60a5fa}.sf-fa-stat.level .sf-fa-stat-val{color:var(--sf-fp-amber-text)}.sf-fa-stat.playtime .sf-fa-stat-val{color:var(--sf-fp-green-text)}.sf-fa-stat.banned .sf-fa-stat-val{color:var(--sf-fp-rose-text)}.sf-fa-section{background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04);border-radius:10px;padding:10px 12px;margin-bottom:10px}.sf-fa-section-title{font-size:12px;font-weight:700;color:#fff;margin-bottom:8px;display:flex;align-items:center;gap:5px}.sf-fa-section-title svg{width:14px;height:14px;stroke:var(--sf-fp-purple-text);stroke-width:2;fill:none}.sf-fa-section-row{display:flex;justify-content:space-between;align-items:center;padding:5px 0;font-size:12px;border-top:1px solid rgba(255,255,255,0.04)}.sf-fa-section-row:first-of-type{border-top:none}.sf-fa-section-key{color:var(--sf-fp-text-secondary)}.sf-fa-section-val{color:#fff;font-weight:600}.sf-fa-link-row{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.sf-fa-link-row .sf-fp-action{flex:1;min-width:100px;margin-top:0}'; // ==================== FA 游戏项(共用)==================== const CSS_FA_GAMES = '.sf-fa-game-item{display:flex;align-items:center;gap:9px;padding:6px 8px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04);border-radius:8px;transition:var(--sf-fp-transition);margin-bottom:4px;cursor:pointer;text-decoration:none!important;color:inherit!important}.sf-fa-game-item:hover{background:rgba(255,255,255,0.05);border-color:rgba(139,92,246,0.25);transform:translateX(2px)}.sf-fa-game-icon{width:40px;height:40px;border-radius:6px;background:rgba(0,0,0,0.4);object-fit:cover;flex-shrink:0;border:1px solid rgba(255,255,255,0.05)}.sf-fa-game-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;overflow:hidden}.sf-fa-game-name{font-size:12px;font-weight:600;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-fa-game-meta{display:flex;align-items:center;gap:6px;font-size:10px;color:var(--sf-fp-text-secondary)}.sf-fa-game-meta .sf-fa-tag{display:inline-flex;align-items:center;gap:2px;background:rgba(255,255,255,0.05);padding:1px 5px;border-radius:4px;color:var(--sf-fp-text-secondary)}.sf-fa-game-meta .sf-fa-tag.recent{background:rgba(16,185,129,0.12);color:var(--sf-fp-green-text)}.sf-fa-game-meta .sf-fa-tag.2w{background:rgba(245,158,11,0.12);color:var(--sf-fp-amber-text)}.sf-fa-game-rank{width:22px;height:22px;border-radius:5px;background:rgba(255,255,255,0.05);font-size:10px;font-weight:700;color:var(--sf-fp-text-secondary);display:flex;align-items:center;justify-content:center;flex-shrink:0}.sf-fa-game-rank.r1{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff}.sf-fa-game-rank.r2{background:linear-gradient(135deg,#94a3b8,#64748b);color:#fff}.sf-fa-game-rank.r3{background:linear-gradient(135deg,#b45309,#92400e);color:#fff}.sf-fa-game-bar{flex:1;height:4px;background:rgba(255,255,255,0.05);border-radius:2px;overflow:hidden;min-width:40px}.sf-fa-game-bar-fill{height:100%;background:linear-gradient(90deg,var(--sf-fp-purple) 0%,var(--sf-fp-blue) 100%);transition:width .4s ease}.sf-fa-donut-wrap{display:flex;align-items:center;gap:12px;padding:8px 0}.sf-fa-donut-svg{flex-shrink:0}.sf-fa-donut-legend{flex:1;display:flex;flex-direction:column;gap:4px;font-size:11px;min-width:0}.sf-fa-donut-legend-item{display:flex;align-items:center;gap:5px;color:var(--sf-fp-text-secondary)}.sf-fa-donut-legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.sf-fa-empty-block{text-align:center;padding:24px 12px;color:var(--sf-fp-text-secondary);font-size:12px;background:rgba(255,255,255,0.02);border-radius:8px;border:1px dashed rgba(255,255,255,0.06)}.sf-fa-empty-block svg{width:32px;height:32px;fill:none;stroke:currentColor;stroke-width:1.5;opacity:0.5;margin-bottom:6px}.sf-fa-delisted-type-pills{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px}.sf-fa-delisted-type-pill{padding:3px 8px;border-radius:14px;background:rgba(255,255,255,0.05);border:1px solid transparent;color:var(--sf-fp-text-secondary);font-size:10px;font-weight:600;cursor:pointer;transition:all .2s ease}.sf-fa-delisted-type-pill.active{background:rgba(139,92,246,0.2);border-color:rgba(139,92,246,0.4);color:var(--sf-fp-purple-text)}.sf-fa-subcats{display:flex;gap:4px;padding:0 0 10px 0;border-bottom:1px solid rgba(255,255,255,0.06);margin-bottom:10px}.sf-fa-subcat{flex:1;display:flex;align-items:center;justify-content:center;gap:5px;padding:8px 10px;border-radius:8px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);color:var(--sf-fp-text-secondary);font-size:11px;font-weight:600;cursor:pointer;transition:all .2s ease;white-space:nowrap}.sf-fa-subcat:hover{background:rgba(255,255,255,0.06);color:#fff}.sf-fa-subcat.active{background:linear-gradient(135deg,rgba(139,92,246,0.25) 0%,rgba(139,92,246,0.1) 100%);border-color:rgba(139,92,246,0.35);color:var(--sf-fp-purple-text)}.sf-fa-subcat svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;flex-shrink:0}.sf-fa-pagination{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.04)}.sf-fa-page-btn{width:28px;height:28px;border-radius:6px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.06);color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center}.sf-fa-page-btn:hover:not(:disabled){background:rgba(139,92,246,0.2);border-color:rgba(139,92,246,0.4)}.sf-fa-page-btn:disabled{opacity:0.3;cursor:not-allowed}.sf-fa-page-info{font-size:11px;color:var(--sf-fp-text-secondary);font-weight:600}'; // ==================== 个人游戏库浮窗(中央模态)==================== const CSS_LIBRARY_PANEL = '#sf-lib-panel{position:fixed!important;top:50%!important;left:50%!important;width:var(--sf-lib-w)!important;max-width:96vw!important;height:var(--sf-lib-h)!important;max-height:92vh!important;background:rgba(17,24,39,0.98)!important;border:1px solid var(--sf-fp-border)!important;border-radius:18px!important;box-shadow:0 30px 100px rgba(0,0,0,0.6)!important;z-index:10001!important;display:flex!important;flex-direction:column;transform:translate(-50%,-48%) scale(0.94)!important;opacity:0!important;visibility:hidden!important;transition:opacity .3s ease,transform .35s cubic-bezier(0.4,0,0.2,1),visibility .3s!important;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--sf-fp-text-primary);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);overflow:hidden}#sf-lib-panel.sfl-open{opacity:1!important;visibility:visible!important;transform:translate(-50%,-50%) scale(1)!important}.sf-lib-header{background:linear-gradient(135deg,rgba(139,92,246,0.2) 0%,rgba(15,23,42,0.85) 100%);padding:12px 18px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--sf-fp-border);flex-shrink:0;position:relative;overflow:hidden}.sf-lib-header::after{content:"";position:absolute;top:-30px;right:-30px;width:100px;height:100px;background:radial-gradient(circle,rgba(139,92,246,0.18) 0%,transparent 70%);pointer-events:none}.sf-lib-title{font-size:15px;font-weight:800;color:#fff;display:flex;align-items:center;gap:8px;position:relative;z-index:1}.sf-lib-title svg{width:18px;height:18px;stroke:var(--sf-fp-purple);stroke-width:2.2;fill:none}.sf-lib-close{width:30px;height:30px;border-radius:8px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;color:#94a3b8;position:relative;z-index:1;flex-shrink:0}.sf-lib-close:hover{background:rgba(244,63,94,0.18);color:var(--sf-fp-rose-text)}.sf-lib-close svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.2;fill:none}.sf-lib-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;padding:12px 18px;background:rgba(15,23,42,0.4);border-bottom:1px solid var(--sf-fp-border);flex-shrink:0}.sf-lib-stat{text-align:center;padding:6px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04);border-radius:8px;transition:var(--sf-fp-transition)}.sf-lib-stat:hover{background:rgba(255,255,255,0.05);transform:translateY(-1px)}.sf-lib-stat-val{font-size:18px;font-weight:800;color:#fff;line-height:1.2}.sf-lib-stat-lbl{font-size:10px;color:var(--sf-fp-text-secondary);margin-top:2px;font-weight:500}.sf-lib-stat.total .sf-lib-stat-val{color:var(--sf-fp-purple-text)}.sf-lib-stat.time .sf-lib-stat-val{color:var(--sf-fp-blue)}.sf-lib-stat.avg .sf-lib-stat-val{color:var(--sf-fp-green-text)}.sf-lib-stat.unplayed .sf-lib-stat-val{color:var(--sf-fp-amber-text)}.sf-lib-tabs{display:flex;align-items:center;gap:4px;padding:8px 12px;background:rgba(15,23,42,0.5);border-bottom:1px solid var(--sf-fp-border);flex-shrink:0;overflow-x:auto;scrollbar-width:thin}.sf-lib-tabs::-webkit-scrollbar{height:3px}.sf-lib-tabs::-webkit-scrollbar-thumb{background:rgba(139,92,246,0.3);border-radius:2px}.sf-lib-tab{display:flex;align-items:center;justify-content:center;gap:5px;padding:7px 12px;border-radius:8px;background:transparent;border:1px solid transparent;color:var(--sf-fp-text-secondary);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.sf-lib-tab:hover{background:rgba(255,255,255,0.05);color:#fff}.sf-lib-tab.active{background:linear-gradient(135deg,rgba(139,92,246,0.25) 0%,rgba(139,92,246,0.1) 100%);border-color:rgba(139,92,246,0.35);color:var(--sf-fp-purple-text)}.sf-lib-tab svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;flex-shrink:0}.sf-lib-body{flex:1;overflow-y:auto;padding:14px 18px;scrollbar-width:thin;scrollbar-color:rgba(139,92,246,0.3) transparent}.sf-lib-body::-webkit-scrollbar{width:5px}.sf-lib-body::-webkit-scrollbar-thumb{background:rgba(139,92,246,0.3);border-radius:3px}.sf-lib-body::-webkit-scrollbar-track{background:transparent}.sf-lib-loading{text-align:center;padding:40px 16px;color:var(--sf-fp-text-secondary);font-size:13px}.sf-lib-loading-spinner{width:32px;height:32px;border:3px solid rgba(139,92,246,0.2);border-top-color:var(--sf-fp-purple);border-radius:50%;animation:sf-spin .8s linear infinite;margin:0 auto 10px}.sf-lib-section{background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04);border-radius:10px;padding:12px;margin-bottom:10px}.sf-lib-section-title{font-size:13px;font-weight:700;color:#fff;margin-bottom:10px;display:flex;align-items:center;gap:6px}.sf-lib-section-title svg{width:14px;height:14px;stroke:var(--sf-fp-purple-text);stroke-width:2;fill:none;flex-shrink:0}.sf-lib-empty-block{text-align:center;padding:30px 16px;color:var(--sf-fp-text-secondary);font-size:12px;background:rgba(255,255,255,0.02);border-radius:8px;border:1px dashed rgba(255,255,255,0.06)}.sf-lib-empty-block svg{width:36px;height:36px;fill:none;stroke:currentColor;stroke-width:1.5;opacity:0.5;margin-bottom:8px}.sf-lib-key-hint{text-align:center;padding:40px 20px;color:var(--sf-fp-text-secondary);font-size:13px;background:rgba(255,255,255,0.02);border-radius:10px;border:1px dashed rgba(255,255,255,0.08);margin:20px 0}.sf-lib-key-hint svg{width:48px;height:48px;fill:none;stroke:var(--sf-fp-amber);stroke-width:1.5;margin-bottom:10px}.sf-lib-key-hint a{color:var(--sf-fp-blue);text-decoration:none;font-weight:600;font-size:12px;display:inline-block;margin-top:6px;padding:4px 10px;background:rgba(59,130,246,0.1);border-radius:6px;border:1px solid rgba(59,130,246,0.3)}.sf-lib-key-hint a:hover{background:rgba(59,130,246,0.2);text-decoration:underline}.sf-lib-key-input-row{display:flex;gap:6px;margin-top:10px}.sf-lib-key-input-row input{flex:1;padding:8px 10px;background:rgba(15,23,42,0.6);border:1px solid var(--sf-fp-border);border-radius:6px;color:#fff;font-size:12px;outline:none;font-family:monospace}.sf-lib-key-input-row input:focus{border-color:var(--sf-fp-blue);box-shadow:0 0 0 2px rgba(59,130,246,0.15)}.sf-lib-key-input-row button{padding:8px 14px;background:linear-gradient(135deg,var(--sf-fp-blue) 0%,var(--sf-fp-blue-strong) 100%);color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s ease;flex-shrink:0}.sf-lib-key-input-row button:hover{filter:brightness(1.15);transform:translateY(-1px)}.sf-lib-playtime-row{display:flex;gap:14px;flex-wrap:wrap}.sf-lib-playtime-col{flex:1;min-width:280px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04);border-radius:10px;padding:12px}.sf-lib-playtime-col .sf-lib-section-title{margin-bottom:10px}.sf-lib-trend-chart{position:relative}.sf-lib-trend-filter{display:flex;gap:0;border:1px solid rgba(255,255,255,0.08);border-radius:6px;overflow:hidden}.sf-lib-trend-btn{background:rgba(0,0,0,0.2);border:none;border-right:1px solid rgba(255,255,255,0.08);border-radius:0;padding:4px 10px;font-size:11px;height:24px;color:#94a3b8;cursor:pointer;transition:all .2s}.sf-lib-trend-btn:last-child{border-right:none}.sf-lib-trend-btn:hover{background:rgba(59,130,246,0.15);color:#f1f5f9}.sf-lib-trend-btn.active{background:rgba(102,192,244,0.2);color:#66c0f4}.sf-lib-trend-svg{display:block;border-radius:6px;overflow:hidden}'; // ==================== 库游戏项(共用 CSS)==================== const CSS_LIB_GAMES = '.sf-lib-game-item{display:flex;align-items:center;gap:10px;padding:7px 10px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04);border-radius:8px;transition:all .2s ease;margin-bottom:5px;cursor:pointer;text-decoration:none!important;color:inherit!important}.sf-lib-game-item:hover{background:rgba(255,255,255,0.05);border-color:rgba(139,92,246,0.25);transform:translateX(2px)}.sf-lib-game-icon{width:44px;height:44px;border-radius:6px;background:rgba(0,0,0,0.4);object-fit:cover;flex-shrink:0;border:1px solid rgba(255,255,255,0.05)}.sf-lib-game-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;overflow:hidden}.sf-lib-game-name{font-size:13px;font-weight:600;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sf-lib-game-meta{display:flex;align-items:center;gap:6px;font-size:10px;color:var(--sf-fp-text-secondary);flex-wrap:wrap}.sf-lib-game-meta .sf-lib-game-time{background:rgba(139,92,246,0.12);color:var(--sf-fp-purple-text);padding:1px 5px;border-radius:4px;font-weight:600}.sf-lib-game-meta .sf-lib-game-last{color:var(--sf-fp-text-secondary)}.sf-lib-game-meta .sf-lib-game-tag{background:rgba(255,255,255,0.05);padding:1px 5px;border-radius:4px;color:var(--sf-fp-text-secondary)}.sf-lib-game-rank{width:24px;height:24px;border-radius:5px;background:rgba(255,255,255,0.05);font-size:11px;font-weight:700;color:var(--sf-fp-text-secondary);display:flex;align-items:center;justify-content:center;flex-shrink:0}.sf-lib-game-rank.r1{background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff}.sf-lib-game-rank.r2{background:linear-gradient(135deg,#94a3b8,#64748b);color:#fff}.sf-lib-game-rank.r3{background:linear-gradient(135deg,#b45309,#92400e);color:#fff}.sf-lib-game-bar{flex:1;height:5px;background:rgba(255,255,255,0.05);border-radius:3px;overflow:hidden;min-width:50px;max-width:90px}.sf-lib-game-bar-fill{height:100%;background:linear-gradient(90deg,var(--sf-fp-purple) 0%,var(--sf-fp-blue) 100%);transition:width .4s ease;border-radius:3px}.sf-lib-delisted-badge{font-size:9px;font-weight:700;padding:2px 6px;border-radius:4px;flex-shrink:0}.sf-lib-delisted-badge.type-delisted{background:rgba(244,67,54,0.2);color:#f87171}.sf-lib-delisted-badge.type-purchase_disabled{background:rgba(255,152,0,0.2);color:#fb923c}.sf-lib-delisted-badge.type-f2p_unavailable{background:rgba(156,39,176,0.2);color:#c084fc}.sf-lib-delisted-badge.type-retail_only{background:rgba(33,150,243,0.2);color:#60a5fa}.sf-lib-delisted-badge.type-test_app{background:rgba(96,125,139,0.2);color:#94a3b8}.sf-lib-timeline-divider{display:flex;align-items:center;gap:8px;margin:14px 0 8px;font-size:11px;color:var(--sf-fp-text-secondary)}.sf-lib-timeline-divider::before,.sf-lib-timeline-divider::after{content:"";flex:1;height:1px;background:rgba(255,255,255,0.06)}.sf-lib-timeline-header{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.2);border-radius:6px;margin-bottom:6px;font-size:11px;font-weight:600}.sf-lib-timeline-label{display:flex;align-items:center;gap:4px}.sf-lib-timeline-count{color:var(--sf-fp-text-secondary);font-weight:500}'; // ==================== 库分页 ==================== const CSS_LIB_PAGINATION = '.sf-lib-pagination{display:flex;align-items:center;justify-content:center;gap:4px;margin-top:10px;flex-wrap:wrap}.sf-lib-page-btn{padding:5px 10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);border-radius:6px;color:var(--sf-fp-text-secondary);font-size:11px;font-weight:600;cursor:pointer;transition:all .2s ease;min-width:30px;text-align:center}.sf-lib-page-btn:hover{background:rgba(139,92,246,0.15);color:var(--sf-fp-purple-text);border-color:rgba(139,92,246,0.3)}.sf-lib-page-btn.active{background:linear-gradient(135deg,var(--sf-fp-purple) 0%,#6d28d9 100%);color:#fff;border-color:transparent}.sf-lib-page-btn:disabled{opacity:.4;cursor:not-allowed;background:rgba(255,255,255,0.02)}.sf-lib-page-info{font-size:11px;color:var(--sf-fp-text-secondary);padding:0 6px}'; // ==================== 绝版游戏增强展示(参考 Steam-License-Classifier)==================== const CSS_DELISTED_ENHANCED = '.sf-delisted-progress{height:5px;background:rgba(255,255,255,0.06);border-radius:3px;overflow:hidden;margin:0 0 10px}.sf-delisted-progress-fill{height:100%;background:linear-gradient(90deg,#22c55e,#66c0f4);border-radius:3px;transition:width .3s ease}.sf-delisted-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:10px}.sf-delisted-search{flex:1;min-width:140px;background:rgba(0,0,0,0.25);border:1px solid rgba(255,255,255,0.08);border-radius:20px;padding:6px 12px;color:#f1f5f9;font-size:12px;outline:none;transition:all .2s}.sf-delisted-search::placeholder{color:#64748b}.sf-delisted-search:focus{border-color:rgba(102,192,244,0.5);box-shadow:0 0 0 2px rgba(102,192,244,0.15)}.sf-delisted-sort-group{display:flex;gap:0;border:1px solid rgba(255,255,255,0.08);border-radius:6px;overflow:hidden}.sf-delisted-sort-btn{background:rgba(0,0,0,0.2)!important;border:none!important;border-right:1px solid rgba(255,255,255,0.08)!important;border-radius:0!important;padding:5px 10px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s}.sf-delisted-sort-btn:last-child{border-right:none!important}.sf-delisted-sort-btn:hover{background:rgba(59,130,246,0.15)!important;color:#f1f5f9!important}.sf-delisted-sort-btn.active{background:rgba(102,192,244,0.2)!important;color:#66c0f4!important}.sf-delisted-view-toggle{display:flex;gap:0;border:1px solid rgba(255,255,255,0.08);border-radius:6px;overflow:hidden}.sf-delisted-view-btn{background:rgba(0,0,0,0.2)!important;border:none!important;border-right:1px solid rgba(255,255,255,0.08)!important;border-radius:0!important;padding:5px 10px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s}.sf-delisted-view-btn:last-child{border-right:none!important}.sf-delisted-view-btn:hover{background:rgba(59,130,246,0.15)!important;color:#f1f5f9!important}.sf-delisted-view-btn.active{background:rgba(102,192,244,0.2)!important;color:#66c0f4!important}.sf-delisted-cover-btn{background:rgba(0,0,0,0.2)!important;border:1px solid rgba(255,255,255,0.08)!important;border-radius:6px!important;padding:5px 8px!important;font-size:11px!important;height:28px!important;color:#94a3b8!important;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center}.sf-delisted-cover-btn:hover{background:rgba(59,130,246,0.15)!important;color:#f1f5f9!important}.sf-delisted-cover-btn.active{background:rgba(102,192,244,0.2)!important;border-color:rgba(102,192,244,0.4)!important;color:#66c0f4!important}.sf-delisted-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px}.sf-delisted-card{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.05);border-radius:8px;padding:10px 12px;transition:all .2s;cursor:pointer;position:relative}.sf-delisted-card:hover{background:rgba(255,255,255,0.06);border-color:rgba(244,67,54,0.25);transform:translateY(-1px)}.sf-delisted-card-name{font-size:12px;font-weight:600;color:#f1f5f9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}.sf-delisted-card-meta{font-size:10px;color:#64748b;display:flex;align-items:center;gap:6px;flex-wrap:wrap}.sf-delisted-card-meta .sf-lib-delisted-badge{font-size:9px}.sf-delisted-list{display:flex;flex-direction:column;gap:4px}.sf-delisted-list-item{display:flex;align-items:center;gap:10px;padding:7px 10px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.04);border-radius:8px;cursor:pointer;transition:all .2s;text-decoration:none!important;color:inherit!important}.sf-delisted-list-item:hover{background:rgba(255,255,255,0.05);border-color:rgba(244,67,54,0.2)}.sf-delisted-list-item.has-cover{gap:12px}.sf-delisted-cover{width:120px;height:45px;border-radius:4px;object-fit:cover;flex-shrink:0;background:rgba(0,0,0,0.3)}.sf-delisted-cover-slot{width:120px;height:45px;flex-shrink:0;border-radius:4px;overflow:hidden}.sf-delisted-cover-slot>div{width:100%;height:100%;border-radius:4px}.sf-delisted-list-item .name{flex:1;font-size:12px;color:#f1f5f9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sf-delisted-list-item .meta{display:flex;align-items:center;gap:6px;flex-shrink:0}.sf-delisted-card-cover{width:100%;height:74px;border-radius:6px;overflow:hidden;margin-bottom:8px;background:rgba(0,0,0,0.3)}.sf-delisted-cover-img{width:100%;height:100%;object-fit:cover;display:block}.sf-delisted-card-cover>div{width:100%;height:100%}.sf-delisted-owned-badge{position:absolute;bottom:6px;right:6px;font-size:9px;padding:2px 6px;border-radius:3px;font-weight:600}.sf-delisted-owned-badge.owned{background:rgba(34,197,94,0.15);color:#22c55e}.sf-delisted-owned-badge.missing{background:rgba(244,67,54,0.12);color:#f44336}'; // ==================== 设置面板(独立模态)==================== const CSS_SETTINGS_PANEL = '#sf-settings-panel{position:fixed!important;top:50%!important;left:50%!important;width:440px!important;max-width:92vw!important;background:rgba(17,24,39,0.98)!important;border:1px solid var(--sf-fp-border)!important;border-radius:18px!important;box-shadow:0 30px 100px rgba(0,0,0,0.6)!important;z-index:10002!important;display:flex!important;flex-direction:column;transform:translate(-50%,-48%) scale(0.94)!important;opacity:0!important;visibility:hidden!important;transition:opacity .3s ease,transform .35s cubic-bezier(0.4,0,0.2,1),visibility .3s!important;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--sf-fp-text-primary);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);overflow:hidden}#sf-settings-panel.sf-settings-open{opacity:1!important;visibility:visible!important;transform:translate(-50%,-50%) scale(1)!important}.sf-settings-header{background:linear-gradient(135deg,rgba(99,102,241,0.2) 0%,rgba(15,23,42,0.85) 100%);padding:14px 18px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--sf-fp-border);flex-shrink:0}.sf-settings-title{font-size:15px;font-weight:800;color:#fff;display:flex;align-items:center;gap:8px}.sf-settings-title svg{width:18px;height:18px;stroke:var(--sf-fp-purple);stroke-width:2.2;fill:none}.sf-settings-close{width:30px;height:30px;border-radius:8px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;color:#94a3b8}.sf-settings-close:hover{background:rgba(244,63,94,0.18);color:var(--sf-fp-rose-text)}.sf-settings-close svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.2;fill:none}.sf-settings-body{padding:18px;overflow-y:auto;max-height:70vh}.sf-settings-group{margin-bottom:18px}.sf-settings-group-title{font-size:12px;font-weight:700;color:#fff;margin-bottom:10px;display:flex;align-items:center;gap:5px}.sf-settings-group-title svg{width:14px;height:14px;stroke:var(--sf-fp-purple-text);stroke-width:2;fill:none}.sf-settings-desc{font-size:11px;color:var(--sf-fp-text-secondary);margin-bottom:8px;line-height:1.5}.sf-settings-input{width:100%;padding:10px 12px;background:rgba(15,23,42,0.6);border:1px solid var(--sf-fp-border);border-radius:8px;color:#fff;font-size:13px;outline:none;font-family:monospace;box-sizing:border-box;transition:all .2s ease}.sf-settings-input:focus{border-color:var(--sf-fp-blue);box-shadow:0 0 0 3px rgba(59,130,246,0.15)}.sf-settings-input::placeholder{color:var(--sf-fp-text-dim)}.sf-settings-actions{display:flex;gap:8px;margin-top:10px}.sf-settings-btn{flex:1;padding:9px 14px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s ease;border:none}.sf-settings-btn.primary{background:linear-gradient(135deg,var(--sf-fp-blue) 0%,var(--sf-fp-blue-strong) 100%);color:#fff}.sf-settings-btn.primary:hover{filter:brightness(1.15);transform:translateY(-1px)}.sf-settings-btn.secondary{background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:var(--sf-fp-text-primary)}.sf-settings-btn.secondary:hover{background:rgba(255,255,255,0.1)}.sf-settings-status{font-size:11px;margin-top:8px;padding:8px 10px;border-radius:6px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);color:var(--sf-fp-text-secondary);display:none}.sf-settings-status.show{display:block}.sf-settings-status.ok{color:#34d399;border-color:rgba(16,185,129,0.2);background:rgba(16,185,129,0.08)}.sf-settings-status.err{color:var(--sf-fp-rose-text);border-color:rgba(244,63,94,0.2);background:rgba(244,63,94,0.08)}.sf-settings-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:10001;opacity:0;visibility:hidden;transition:all .3s ease;backdrop-filter:blur(2px)}.sf-settings-overlay.sf-show{opacity:1;visibility:visible}'; // ==================== 用户信息弹窗 ==================== const CSS_USERINFO = '.sf-userinfo-popup{position:absolute!important;top:100%!important;left:0!important;width:280px!important;background:rgba(23,26,33,.98)!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:12px!important;padding:16px!important;z-index:10000!important;box-shadow:0 8px 32px rgba(0,0,0,.6)!important;opacity:0!important;visibility:hidden!important;transform:translateY(-10px)!important;transition:all .2s ease!important}.sf-userinfo-popup.visible{opacity:1!important;visibility:visible!important;transform:translateY(0)!important}.sf-ui-header{display:flex!important;align-items:center!important;gap:12px!important;margin-bottom:12px!important}.sf-ui-avatar{width:48px!important;height:48px!important;border-radius:8px!important}.sf-ui-name{font-size:14px!important;font-weight:600!important;color:#fff!important}.sf-ui-level{font-size:12px!important;color:var(--sf-dim)!important}.sf-ui-stats{display:grid!important;grid-template-columns:repeat(2,1fr)!important;gap:8px!important;margin-bottom:12px!important}.sf-ui-stat{padding:8px!important;background:rgba(255,255,255,.05)!important;border-radius:8px!important;text-align:center!important}.sf-ui-stat-value{font-size:16px!important;font-weight:600!important;color:var(--sf-accent)!important}.sf-ui-stat-label{font-size:11px!important;color:var(--sf-dim)!important;margin-top:2px!important}.sf-ui-actions{display:flex!important;gap:8px!important}.sf-ui-action-btn{flex:1!important;padding:8px!important;border-radius:6px!important;background:rgba(102,192,244,.15)!important;border:1px solid rgba(102,192,244,.3)!important;color:var(--sf-accent)!important;font-size:12px!important;cursor:pointer!important;transition:all .2s!important;text-align:center!important}.sf-ui-action-btn:hover{background:rgba(102,192,244,.25)!important}.sf-ui-loading{text-align:center!important;padding:20px!important;color:var(--sf-dim)!important}.sf-ui-loading-spinner{width:24px!important;height:24px!important;border:3px solid rgba(102,192,244,.2);border-top-color:var(--sf-accent);border-radius:50%;animation:sf-spin .8s linear infinite;margin:0 auto 8px}.sf-ui-flag{width:16px;height:12px;border-radius:2px;display:inline-block;vertical-align:middle;margin-left:4px}'; // ==================== 左侧分类导航 ==================== const CSS_SIDEBAR = '#sf-sidebar-nav{position:fixed!important;left:0!important;top:0!important;width:200px!important;height:100vh!important;background:rgba(23,26,33,.97)!important;backdrop-filter:blur(12px)!important;-webkit-backdrop-filter:blur(12px)!important;border-right:1px solid rgba(255,255,255,.08)!important;z-index:9990!important;transform:translateX(-100%)!important;transition:transform .35s cubic-bezier(.4,0,.2,1)!important;overflow-y:auto!important;overflow-x:hidden!important;padding-top:60px!important;box-shadow:4px 0 20px rgba(0,0,0,.3)!important}#sf-sidebar-nav.sf-sidebar-open{transform:translateX(0)!important}#sf-sidebar-nav::-webkit-scrollbar{width:4px!important}#sf-sidebar-nav::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15)!important;border-radius:2px!important}#sf-sidebar-nav::-webkit-scrollbar-track{background:transparent!important}.sf-sidebar-title{padding:12px 18px 16px!important;font-size:14px!important;font-weight:700!important;color:#fff!important;letter-spacing:.5px!important;display:flex!important;align-items:center!important;gap:8px!important}.sf-sidebar-title svg{width:18px!important;height:18px!important;fill:var(--sf-accent)!important}.sf-sidebar-item{display:flex!important;align-items:center!important;gap:10px!important;padding:10px 18px!important;color:var(--sf-text)!important;font-size:13px!important;font-weight:500!important;cursor:pointer!important;transition:all .2s ease!important;border-left:3px solid transparent!important;white-space:nowrap!important}.sf-sidebar-item:hover{background:var(--sf-hover)!important;color:#fff!important}.sf-sidebar-item.active{background:rgba(102,192,244,.1)!important;color:var(--sf-accent)!important;border-left-color:var(--sf-accent)!important}.sf-sidebar-item svg{width:16px!important;height:16px!important;fill:currentColor!important;flex-shrink:0!important}.sf-sidebar-item .sf-sidebar-count{margin-left:auto!important;padding:1px 6px!important;background:rgba(255,255,255,.1)!important;border-radius:8px!important;font-size:10px!important;color:var(--sf-dim)!important}.sf-sidebar-item.active .sf-sidebar-count{background:rgba(102,192,244,.2)!important;color:var(--sf-accent)!important}.sf-sidebar-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9989;opacity:0;visibility:hidden;transition:all .3s ease;backdrop-filter:blur(2px)}.sf-sidebar-overlay.sf-show{opacity:1;visibility:visible}body.sf-sidebar-mode .responsive_page_content{margin-left:200px!important;margin-right:0!important;max-width:calc(100vw - 232px)!important;width:auto!important;box-sizing:border-box!important;transition:margin-left .35s cubic-bezier(.4,0,.2,1),max-width .35s cubic-bezier(.4,0,.2,1)!important}body.sf-sidebar-mode .responsive_page_frame{padding-left:0!important}body.sf-sidebar-mode #blotter_page{max-width:none!important}'; // ==================== 兼容 Steam 游戏库展示个人信息面板 ==================== const CSS_COMPAT_SGIS = '/* 兼容 Steam 游戏库展示 (sgis) 个人信息面板:面板打开时瀑布流浮窗让出右侧侧边栏 */body.sgis-open #sf-wf-overlay{right:440px!important;left:0!important;width:calc(100vw - 440px)!important;transition:opacity .35s ease,visibility .35s ease,right .4s cubic-bezier(.4,0,.2,1),width .4s cubic-bezier(.4,0,.2,1)!important}body.sgis-open #sf-wf-overlay .sf-wf-header{left:0!important;width:100%!important}body.sgis-open #sf-wf-overlay .sf-wf-body{max-width:calc(100vw - 488px)!important;margin:0!important;padding-left:24px!important}body.sgis-open #sf-wf-overlay .sf-wf-grid{justify-content:flex-start!important}body.sgis-open #sf-waterfall-btn,body.sgis-open #sf-friend-btn,body.sgis-open #sf-library-btn,body.sgis-open #sf-settings-btn,body.sgis-open #sf-top-btn{right:470px!important}@media(max-width:600px){body.sgis-open #sf-wf-overlay{right:0!important;width:100vw!important}body.sgis-open #sf-wf-overlay .sf-wf-body{max-width:none!important;padding-left:12px!important}body.sgis-open #sf-waterfall-btn,body.sgis-open #sf-friend-btn,body.sgis-open #sf-library-btn,body.sgis-open #sf-settings-btn,body.sgis-open #sf-top-btn{right:16px!important}}'; // ==================== 图片浮层 ==================== const CSS_OVERLAY = '.sf-overlay{position:fixed!important;inset:0!important;z-index:99999!important;background:rgba(0,0,0,.92)!important;display:flex!important;flex-direction:column!important;align-items:center!important;justify-content:center!important;opacity:0!important;visibility:hidden!important;transition:all .3s ease!important}.sf-overlay.sf-show{opacity:1!important;visibility:visible!important}.sf-overlay-img-wrap{flex:1!important;display:flex;align-items:center;justify-content:center!important;width:100%!important;padding:20px!important;position:relative!important;overflow:hidden!important}.sf-overlay-img{max-width:calc(100vw - 40px)!important;max-height:calc(100vh - 120px)!important;object-fit:contain!important;border-radius:4px!important;user-select:none!important;transition:transform .2s ease!important;cursor:grab!important}.sf-overlay-img:active{cursor:grabbing!important}.sf-overlay-img.sf-can-drag{cursor:grab!important}.sf-overlay-img.sf-dragging{cursor:grabbing!important}.sf-overlay-toolbar{width:100%!important;padding:12px 24px!important;background:rgba(0,0,0,.6)!important;backdrop-filter:blur(10px)!important;display:flex!important;align-items:center!important;justify-content:center!important;gap:20px!important;flex-shrink:0!important}.sf-overlay-action{display:flex!important;flex-direction:column!important;align-items:center!important;gap:4px!important;color:#8f98a0!important;font-size:11px!important;cursor:pointer!important;transition:color .2s!important;padding:6px 12px!important;border-radius:8px!important;background:transparent!important;border:none!important;font-family:inherit!important}.sf-overlay-action:hover{color:#66c0f4!important;background:rgba(255,255,255,.05)!important}.sf-overlay-action:disabled,.sf-overlay-action[disabled]{opacity:.35!important;cursor:not-allowed!important}.sf-overlay-action svg{width:22px!important;height:22px!important;fill:currentColor!important}.sf-overlay-close{position:fixed!important;top:16px!important;right:16px!important;width:40px!important;height:40px!important;border-radius:50%!important;background:rgba(255,255,255,.1)!important;border:none!important;cursor:pointer!important;display:flex!important;align-items:center!important;justify-content:center!important;color:#fff!important;transition:all .2s!important;z-index:100000!important}.sf-overlay-close:hover{background:rgba(244,67,54,.4)!important}.sf-overlay-close svg{width:20px!important;height:20px!important;fill:currentColor!important}.sf-overlay-nav{position:fixed!important;top:50%!important;transform:translateY(-50%)!important;width:48px!important;height:48px!important;border-radius:50%!important;background:rgba(0,0,0,.5)!important;border:1px solid rgba(255,255,255,.1)!important;cursor:pointer!important;display:flex!important;align-items:center!important;justify-content:center!important;color:#fff!important;transition:all .2s!important;z-index:100000!important}.sf-overlay-nav:hover:not([disabled]){background:rgba(102,192,244,.3)!important;transform:translateY(-50%) scale(1.1)!important}.sf-overlay-nav:disabled{opacity:.3!important;cursor:not-allowed!important}.sf-overlay-nav svg{width:24px!important;height:24px!important;fill:currentColor!important}.sf-overlay-nav-prev{left:16px!important}.sf-overlay-nav-next{right:16px!important}.sf-overlay-counter{position:fixed!important;top:20px!important;left:50%!important;transform:translateX(-50%)!important;color:#fff!important;font-size:14px!important;font-weight:600!important;background:rgba(0,0,0,.5)!important;padding:4px 12px!important;border-radius:12px!important;z-index:100000!important}.sf-ov-zoom{position:fixed!important;bottom:80px!important;left:20px!important;color:#fff!important;font-size:13px!important;font-weight:600!important;background:rgba(0,0,0,.5)!important;padding:4px 10px!important;border-radius:8px!important;z-index:100000!important}.sf-ov-hint{position:fixed!important;bottom:80px!important;right:20px!important;color:#8f98a0!important;font-size:12px!important;background:rgba(0,0,0,.5)!important;padding:4px 10px!important;border-radius:8px!important;opacity:0!important;transition:opacity .3s ease!important;z-index:100000!important}.sf-ov-hint.sf-show{opacity:1!important}.sf-comments-collapsed{display:none!important}.sf-comments-expanded{display:block!important}.sf-collapse-hint{padding:10px 14px!important;background:rgba(0,0,0,.3)!important;border-radius:8px!important;margin:12px 16px!important;display:flex!important;align-items:center!important;justify-content:space-between!important;cursor:pointer!important;font-size:13px!important;color:var(--sf-text)!important;transition:all .2s ease!important}.sf-collapse-hint:hover{background:rgba(0,0,0,.4)!important;color:#fff!important}.sf-collapse-hint-text{display:flex!important;align-items:center!important;gap:8px!important}.sf-collapse-hint-text svg{width:16px!important;height:16px!important;fill:var(--sf-accent)!important;flex-shrink:0!important}.sf-collapse-hint-arrow{transition:transform .25s ease!important;display:flex!important;align-items:center!important}.sf-collapse-hint-arrow svg{width:18px!important;height:18px!important;fill:var(--sf-text)!important}.sf-collapse-hint.sf-expanded .sf-collapse-hint-arrow{transform:rotate(180deg)!important}'; // ==================== 页面增强 ==================== const CSS_PAGE_ENHANCED = '.sf-page-enhanced .rightCol,.sf-page-enhanced .rightDetails,.sf-page-enhanced [class*="rightCol"],.sf-page-enhanced [class*="rightDetails"]{display:none!important}.sf-page-enhanced .workshopItemPreviewHolder,.sf-page-enhanced #sharedfiles_previewarea,.sf-page-enhanced [class*="previewHolder"],.sf-page-enhanced [class*="previewArea"]{max-width:100%!important;width:100%!important;margin:0 auto!important}.sf-page-enhanced .actualScreenshotImg,.sf-page-enhanced #screenshotImage,.sf-page-enhanced .workshopItemPreviewHolder img{max-width:100%!important;width:auto!important;height:auto!important;cursor:zoom-in!important;display:block!important;margin:0 auto!important}'; // ==================== Toast ==================== const CSS_TOAST = '.sf-toast{position:fixed!important;top:24px!important;left:50%!important;transform:translateX(-50%) translateY(-10px)!important;padding:10px 24px!important;border-radius:8px!important;font-size:14px!important;font-weight:500!important;z-index:100001!important;opacity:0!important;transition:all .3s ease!important;pointer-events:none!important;box-shadow:0 4px 16px rgba(0,0,0,.5)!important}.sf-toast.sf-toast--visible{opacity:1!important;transform:translateX(-50%) translateY(0)!important}.sf-toast--info{background:#2a475e!important;color:#c7d5e0!important;border:1px solid #4a6a7a!important}.sf-toast--warn{background:rgba(234,179,8,.9)!important;color:#1a1a1a!important}.sf-toast--error{background:rgba(244,67,54,.9)!important;color:#fff!important}'; // ==================== 移动端 ==================== const CSS_MOBILE = '@media(max-width:600px){#sf-waterfall-btn,#sf-friend-btn,#sf-library-btn,#sf-settings-btn,#sf-top-btn{right:16px!important;width:42px!important;height:42px!important}#sf-waterfall-btn{bottom:14px!important}#sf-friend-btn{bottom:62px!important}#sf-library-btn{bottom:110px!important}#sf-settings-btn{bottom:158px!important}#sf-top-btn{bottom:206px!important}#sf-filter-bar{padding:8px 10px!important;gap:6px!important}.sf-filter-btn{padding:6px 10px!important;font-size:12px!important}.sf-filter-label{display:none!important}.sf-wf-grid{flex-direction:column!important}.sf-wf-col{max-width:none!important;min-width:0!important}.sf-wf-body{padding:12px!important}#sf-sidebar-nav{width:180px!important;padding-top:50px!important}body.sf-sidebar-mode .responsive_page_content{margin-left:180px!important;margin-right:0!important;max-width:calc(100vw - 196px)!important;width:auto!important;box-sizing:border-box!important}body.sf-sidebar-mode #blotter_page{max-width:none!important}#sf-friend-panel{width:calc(100vw - 32px)!important;right:16px!important;max-height:75vh!important}#sf-fa-panel{width:100vw!important;max-width:100vw!important}#sf-lib-panel{width:96vw!important;height:90vh!important}#sf-settings-panel{width:92vw!important}}'; // ==================== 合并所有 CSS ==================== const CSS = [ CSS_VARS, CSS_LAYOUT, CSS_CARDS, CSS_SCREENSHOTS, CSS_GAMES, CSS_WORKSHOP, CSS_CONTROLS, CSS_DATE_STATUS, CSS_MODAL, CSS_FILTER, CSS_LOAD_EMPTY, CSS_FAB_BASE, CSS_FAB_EACH, CSS_FAB_HOVER, CSS_FAB_LABEL, CSS_WATERFALL, CSS_WF_OVERRIDES, CSS_WF_LOADING, CSS_FRIEND_PANEL, CSS_FRIEND_OWN, CSS_FRIEND_CATEGORY, CSS_FRIEND_ITEM, CSS_FRIEND_MISC, CSS_FRIEND_FOOTER, CSS_FA_PANEL, CSS_FA_OVERVIEW, CSS_FA_GAMES, CSS_LIBRARY_PANEL, CSS_LIB_GAMES, CSS_LIB_PAGINATION, CSS_DELISTED_ENHANCED, CSS_USERINFO, CSS_SETTINGS_PANEL, CSS_SIDEBAR, CSS_COMPAT_SGIS, CSS_OVERLAY, CSS_PAGE_ENHANCED, CSS_TOAST, CSS_MOBILE ].join(''); // ==================== SVG 图标 ==================== const ICONS = { all: '', screenshot: '', game: '', review: '', workshop: '', favorite: '', wishlist: '', achievement: '', close: '', expand: '', collapse: '', fullscreen: '', comment: '', chevronLeft: '', chevronRight: '', chevronDown: '', zoomIn: '', zoomOut: '', refresh: '', download: '', history: '', top: '', waterfall: '', sidebar: '', topbar: '', library: '', activity: '', chartBar: '', shield: '', key: '', trending: '', trash: '', settings: '', friends: '', sync: '', award: '', }; // ==================== SVG 国旗库(精选 50+ 国家)==================== const FLAGS = { CN:'', US:'', JP:'', KR:'', TW:'', HK:'', MO:'', GB:'', DE:'', FR:'', RU:'', AU:'', CA:'', BR:'', IN:'', SG:'', MY:'', TH:'', VN:'', ID:'', PH:'', NZ:'', SE:'', NO:'', FI:'', DK:'', NL:'', IT:'', ES:'', PL:'', UA:'', TR:'', SA:'', AE:'', EG:'', ZA:'', IL:'', MX:'', AR:'', CL:'', CH:'', AT:'', BE:'', PT:'', GR:'', CZ:'', HU:'', RO:'', IE:'', LT:'', LV:'', EE:'', IS:'', }; function flagSvg(cc) { if (!cc || cc.length !== 2) return ''; const uc = cc.toUpperCase(); const svg = FLAGS[uc]; const style = 'display:inline-block;width:14px;height:10px;vertical-align:middle;flex-shrink:0;border-radius:1px;'; if (svg) return svg.replace('${uc}`; } // ==================== 工具函数 ==================== const $ = sel => document.querySelector(sel); const $$ = sel => document.querySelectorAll(sel); const esc = s => { if (s == null) return ''; const d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }; const el = (tag, attrs, children) => { const e = document.createElement(tag); if (attrs) { const { dataset, ...rest } = attrs; Object.assign(e, rest); if (dataset) Object.assign(e.dataset, dataset); } if (children) e.innerHTML = children; return e; }; const debounce = (fn, wait) => { let t; return function(...args) { clearTimeout(t); t = setTimeout(() => fn.apply(this, args), wait); }; }; const findShortestCol = cols => { let shortest = cols[0], minH = shortest.offsetHeight; for (let i = 1; i < cols.length; i++) { if (cols[i].offsetHeight < minH) { shortest = cols[i]; minH = cols[i].offsetHeight; } } return shortest; }; const sleep = ms => new Promise(r => setTimeout(r, ms)); // ==================== 统一 Toast 系统 ==================== let _toastTimer = null; let _currentToast = null; function showToast(msg, type = 'info', ms = 1500) { if (_currentToast) { _currentToast.remove(); _currentToast = null; } if (_toastTimer) { clearTimeout(_toastTimer); _toastTimer = null; } const t = document.createElement('div'); t.className = `sf-toast sf-toast--${type}`; t.textContent = msg; document.body.appendChild(t); _currentToast = t; requestAnimationFrame(() => requestAnimationFrame(() => t.classList.add('sf-toast--visible'))); _toastTimer = setTimeout(() => { t.classList.remove('sf-toast--visible'); setTimeout(() => { t.remove(); if (_currentToast === t) _currentToast = null; }, 300); }, ms); } // ==================== 资源清理钩子 ==================== const cleanupFns = []; function registerCleanup(fn) { cleanupFns.push(fn); } window.addEventListener('beforeunload', () => { cleanupFns.forEach(fn => { try { fn(); } catch (e) { /* ignore */ } }); cleanupFns.length = 0; }); // ==================== 配置常量 ==================== const CONFIG = { INITIAL_LOAD: 100, LOAD_MORE_STEP: 50, CACHE_TTL: 300000, CACHE_MAX_SIZE: 200, REQUEST_TIMEOUT: 10000, WF_BATCH_SIZE: 20, WF_LOAD_THRESHOLD: 800, WF_MAX_SHOW: 50, MAIN_MAX_SHOW: 8, OBSERVER_DEBOUNCE: 100, STEAM_API_BASE: 'https://api.steampowered.com', DELISTED_DB_URL: 'https://xget.xi-xu.me/gh/SmallRob/steam-namespace/raw/refs/heads/main/data/steam_delisted_apps.json', ANNUAL_AAA_URL: 'https://xget.xi-xu.me/gh/SmallRob/steam-namespace/raw/refs/heads/main/data/2026.json', STEAMAPI_BATCH: 100, }; const STEAM_ORIGINAL_IMAGE_SIZE = 5000; const GAME_CAPSULE_URL = 'https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/capsule_231x87.jpg'; // 多 CDN 基址(参考 steam-game-library-viewer lib core:cloudflare → akamai → shared) const CDN_BASES = [ 'https://cdn.cloudflare.steamstatic.com/steam/apps/', 'https://cdn.akamai.steamstatic.com/steam/apps/', 'https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/', ]; const CDN_BASE = CDN_BASES[0]; const SVG_PLACEHOLDER = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='231' height='87'%3E%3Crect fill='%23333' width='231' height='87'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%23999' font-size='14'%3ENo Image%3C/text%3E%3C/svg%3E`; const FALLBACK_CAPSULE_PATHS = [ 'capsule_231x87.jpg', 'capsule_231x87.png', 'header.jpg', 'capsule_184x69.jpg', 'capsule_467x181.jpg', ]; const GAME_ICON_URL = appid => `https://cdn.cloudflare.steamstatic.com/steam/apps/${appid}/capsule_sm_120.jpg`; const GAME_STORE_URL = appid => `https://store.steampowered.com/app/${appid}`; // 成功 URL 内存缓存(参考 lib core _posterGoodUrl:命中后直接用,避免重走回退链) const _coverGoodUrl = new Map(); function recordCoverGood(appid, url) { if (appid && url) _coverGoodUrl.set(String(appid), url); } function getCoverGood(appid) { return _coverGoodUrl.get(String(appid)); } // 多 CDN 回退链构造(参考 lib core coverChainUrls) function coverChainUrls(appid) { const id = String(appid); const chain = []; for (const base of CDN_BASES) { for (const path of ['capsule_231x87.jpg', 'header.jpg']) { chain.push(base + id + '/' + path); } } return chain; } // 占位 div 替换(参考 lib core _posterImgToPlaceholder:失败时用 div 占位而非 display:none,避免错位) function coverImgToPlaceholder(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;background:rgba(0,0,0,0.3);border-radius:6px'; holder.textContent = '无封面'; img.parentElement.replaceChild(holder, img); } // 封面图 API 缓存与去重 const _capsuleCache = new Map(); const _capsulePending = new Set(); const _capsuleWaiters = new Map(); function fetchCapsuleFromAPI(appid) { return new Promise((resolve, reject) => { if (_capsuleCache.has(appid)) { resolve(_capsuleCache.get(appid)); return; } if (_capsulePending.has(appid)) { if (!_capsuleWaiters.has(appid)) _capsuleWaiters.set(appid, []); _capsuleWaiters.get(appid).push({ resolve, reject }); return; } _capsulePending.add(appid); if (!_capsuleWaiters.has(appid)) _capsuleWaiters.set(appid, []); _capsuleWaiters.get(appid).push({ resolve, reject }); const apiUrl = `https://store.steampowered.com/api/appdetails?appids=${appid}&filters=basic`; const doRequest = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : null; const notifyWaiters = (value, isError) => { const waiters = _capsuleWaiters.get(appid) || []; _capsuleWaiters.delete(appid); waiters.forEach(w => { if (isError) w.reject(); else w.resolve(value); }); }; if (doRequest) { doRequest({ method: 'GET', url: apiUrl, onload: (resp) => { let url = ''; try { const json = JSON.parse(resp.responseText); const data = json[appid]; if (data && data.success && data.data) { // filters=basic 同时返回 capsule_image 与 header_image,优先 capsule url = data.data.capsule_image || data.data.header_image || ''; } } catch (e) { url = ''; } _capsuleCache.set(appid, url); if (url) recordCoverGood(appid, url); _capsulePending.delete(appid); notifyWaiters(url, !url); }, onerror: () => { _capsuleCache.set(appid, ''); _capsulePending.delete(appid); notifyWaiters('', true); }, ontimeout: () => { _capsuleCache.set(appid, ''); _capsulePending.delete(appid); notifyWaiters('', true); }, }); } else { fetch(apiUrl) .then(r => r.json()) .then(json => { const data = json[appid]; const url = (data && data.success && data.data && data.data.capsule_image) ? data.data.capsule_image : ''; _capsuleCache.set(appid, url); _capsulePending.delete(appid); notifyWaiters(url, !url); }) .catch(() => { _capsuleCache.set(appid, ''); _capsulePending.delete(appid); notifyWaiters('', true); }); } }); } function loadCapsuleWithFallback(img, appid) { if (!img || !appid) return; // 1. 命中成功缓存直接用 const good = getCoverGood(appid); const chain = good ? [good] : coverChainUrls(appid); let idx = 0; // 2. 回退链走完后走 API 兜底 function tryApi() { fetchCapsuleFromAPI(appid).then(url => { if (url) { img.onload = () => recordCoverGood(appid, img.src); img.onerror = () => { img.onload = null; img.onerror = null; coverImgToPlaceholder(img); }; img.src = url; } else { coverImgToPlaceholder(img); } }).catch(() => coverImgToPlaceholder(img)); } function tryNext() { if (idx >= chain.length) { tryApi(); return; } img.onload = () => recordCoverGood(appid, img.src); img.onerror = () => { img.onload = null; idx++; tryNext(); }; img.src = chain[idx]; } tryNext(); } function extractAppidFromContext(logoElement) { let m; const dataAppid = logoElement.getAttribute('data-appid'); if (dataAppid) return dataAppid; m = (logoElement.getAttribute('href') || '').match(/\/app\/(\d+)/); if (m) return m[1]; const innerA = logoElement.querySelector('a[href*="/app/"]'); if (innerA) { m = (innerA.getAttribute('href') || '').match(/\/app\/(\d+)/); if (m) return m[1]; } const block = logoElement.closest('.blotter_gamepurchase'); if (!block) return null; const usedAppids = new Set(); block.querySelectorAll('.blotter_gamepurchase_logo').forEach(l => { if (l === logoElement) return; const a = l.getAttribute('data-appid'); if (a) usedAppids.add(a); const h = (l.getAttribute('href') || '').match(/\/app\/(\d+)/); if (h) usedAppids.add(h[1]); }); const detailLinks = block.querySelectorAll('.blotter_gamepurchase_details a[href*="/app/"]'); for (const link of detailLinks) { m = (link.getAttribute('href') || '').match(/\/app\/(\d+)/); if (m && !usedAppids.has(m[1])) { logoElement.setAttribute('data-appid', m[1]); return m[1]; } } return null; } function fixGamePurchaseImages() { $$('.blotter_gamepurchase_logo').forEach(logo => { if (logo.dataset.sfImgFixed) return; logo.dataset.sfImgFixed = '1'; const appid = extractAppidFromContext(logo); if (!appid) return; const img = logo.querySelector('img'); const inlineSvg = logo.querySelector('svg'); if (img) { const src = img.src || ''; const isSvgDataUri = src.startsWith('data:image/svg'); const isSvgFile = /\.svg(\?|$)/i.test(src); const isEmpty = !src || src === 'about:blank'; if (isSvgDataUri || isSvgFile || isEmpty) { loadCapsuleWithFallback(img, appid); } else { img.addEventListener('error', () => loadCapsuleWithFallback(img, appid), { once: true }); } } else if (inlineSvg) { inlineSvg.remove(); const newImg = document.createElement('img'); newImg.alt = `Game #${appid}`; newImg.loading = 'lazy'; logo.appendChild(newImg); loadCapsuleWithFallback(newImg, appid); } }); } // ==================== 状态管理 ==================== const state = { currentFilter: 'all', visibleCount: 0, totalBlocks: [], blockTypes: new WeakMap(), wfOpen: false, wfBlocks: [], wfLoadedCount: 0, wfAllLoaded: false, layoutMode: (() => { try { return localStorage.getItem('sf-layout-mode') || 'topbar'; } catch (e) { return 'topbar'; } })(), sidebarVisible: false, // === v4.0/v4.1 新增 === apiKey: '', mySteamId: '', delistedDB: null, annualAaaDB: null, // 好友浮窗内每个好友的扩展信息(游戏数 / 封禁 / 等级) friendExtras: new Map(), // 当前打开的好友动态浮窗状态 faPanel: { open: false, steamId: '', tab: 'overview', subcat: 'all', data: null, delisted: null, annual: null }, // 当前打开的个人游戏库状态:library=全部游戏, recent=最近游玩, playtime=时长, activity=入库列表, delisted=绝版收藏 libPanel: { open: false, tab: 'library', data: null, delisted: null, activity: null, recent: null, allGames: null, annual: null }, // 独立设置窗口状态 settingsPanel: { open: false, pendingAction: null }, // 好友浮窗数据缓存 fpData: null, }; // ==================== 缓存管理(带 LRU 简化)==================== const cache = { data: new Map(), get(key) { const item = this.data.get(key); if (!item) return null; if (Date.now() - item.timestamp > CONFIG.CACHE_TTL) { this.data.delete(key); return null; } this.data.delete(key); this.data.set(key, item); return item.value; }, set(key, value) { if (this.data.size >= CONFIG.CACHE_MAX_SIZE) { const firstKey = this.data.keys().next().value; if (firstKey !== undefined) this.data.delete(firstKey); } this.data.set(key, { value, timestamp: Date.now() }); }, clear() { this.data.clear(); }, }; // ==================== Steam API Key 与 SteamID 管理(持久化)==================== function getApiKey() { if (state.apiKey) return state.apiKey; try { state.apiKey = (typeof GM_getValue === 'function' ? GM_getValue('sf_api_key', '') : localStorage.getItem('sf_api_key') || '') || ''; } catch (e) { state.apiKey = ''; } return state.apiKey; } function setApiKey(key) { state.apiKey = key || ''; try { if (typeof GM_setValue === 'function') GM_setValue('sf_api_key', state.apiKey); else localStorage.setItem('sf_api_key', state.apiKey); } catch (e) { /* ignore */ } } function getStoredSteamId() { try { return (typeof GM_getValue === 'function' ? GM_getValue('sf_steam_id', '') : localStorage.getItem('sf_steam_id') || '') || ''; } catch (e) { return ''; } } function setStoredSteamId(id) { try { if (typeof GM_setValue === 'function') GM_setValue('sf_steam_id', id || ''); else localStorage.setItem('sf_steam_id', id || ''); } catch (e) { /* ignore */ } } function miniIdToSteamId(miniId) { if (!miniId || !/^\d+$/.test(String(miniId))) return ''; try { const sid = BigInt(miniId) + 76561197960265728n; const str = String(sid); return /^\d{17}$/.test(str) ? str : ''; } catch (e) { return ''; } } function getMySteamId() { if (state.mySteamId) return state.mySteamId; // 优先使用用户手动配置的 SteamID const stored = getStoredSteamId(); if (/^\d{17}$/.test(stored)) { state.mySteamId = stored; return stored; } // 其次用 activity 页面 URL 的 steamid const m = (window.location.pathname || '').match(/\/(?:id|profiles)\/([^/]+)/); if (m) { const id = m[1]; if (/^\d{17}$/.test(id)) { state.mySteamId = id; return state.mySteamId; } } // 自定义 URL 或未在 URL 中暴露数字 ID:只能从当前用户专属元素获取 // 1) g_steamID 全局变量 const gsid = (typeof window.g_steamID !== 'undefined' && window.g_steamID) || (typeof window.g_steamid !== 'undefined' && window.g_steamid) || ''; if (/^\d{17}$/.test(gsid)) { state.mySteamId = gsid; return gsid; } // 2) 当前用户头像/链接的 data-miniprofile(好友列表面板 .webchat_own_avatar 或好友列表入口) const ownMiniSelectors = [ '.webchat_own_avatar [data-miniprofile]', '.friendslist_entry_content .linkFriend[data-miniprofile]', '#global_actions .user_avatar_frame[data-miniprofile]', '#account_pulldown [data-miniprofile]', '.account_name [data-miniprofile]', 'a[href="/id/"][data-miniprofile]', 'a[href="/profiles/"][data-miniprofile]', ]; for (const sel of ownMiniSelectors) { const el = document.querySelector(sel); const mini = el?.dataset?.miniprofile || el?.getAttribute('data-miniprofile'); const sid = mini ? miniIdToSteamId(mini) : ''; if (/^\d{17}$/.test(sid)) { state.mySteamId = sid; return sid; } } // 3) 当前用户专属链接(排除可能匹配到好友的通用 a[href*="/profiles/"]) const ownLinkSelectors = [ '.webchat_own_avatar a[href*="/profiles/"]', '.friendslist_entry_content .linkFriend[href*="/profiles/"]', '#global_actions a[href*="/profiles/"]', '#account_pulldown a[href*="/profiles/"]', ]; for (const sel of ownLinkSelectors) { const el = document.querySelector(sel); const href = el?.getAttribute('href') || ''; const match = href.match(/profiles\/(\d{17})/); if (match && /^\d{17}$/.test(match[1])) { state.mySteamId = match[1]; return match[1]; } } return state.mySteamId; } // ==================== 智能并发池(参考 1.0.16,遇 429 自动降速)==================== async function runWithConcurrency(items, limit, worker) { const results = new Array(items.length); let cursor = 0, active = 0; return new Promise((resolve) => { const launch = () => { while (active < limit && cursor < items.length) { const idx = cursor++; active++; Promise.resolve() .then(() => worker(items[idx], idx)) .then(r => { results[idx] = r; }) .catch(err => { results[idx] = { error: err }; }) .finally(() => { active--; if (cursor < items.length) launch(); else if (active === 0) resolve(results); }); } }; launch(); }); } // ==================== Steam Web API 请求(带超时 + 简单重试)==================== async function steamApi(path, params = {}, opts = {}) { const url = new URL(path.startsWith('http') ? path : `${CONFIG.STEAM_API_BASE}${path}`); Object.entries(params).forEach(([k, v]) => { if (v != null) url.searchParams.set(k, v); }); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), opts.timeout || CONFIG.REQUEST_TIMEOUT); try { const resp = await fetch(url.href, { signal: controller.signal, credentials: 'omit' }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); return await resp.json(); } finally { clearTimeout(timeout); } } // ==================== 游戏中文名获取(参考 steam-friend-manager-1.1.2 fetchGameZhName) ==================== const SF_GAME_NAME_KEY = 'sf_game_name_cache'; const SF_NAME_CACHE_TTL = 30 * 864e5; // 30天过期 let _sfGameNameCache = null; const _sfGameNamePending = new Map(); function isZhLocale() { const l = (document.documentElement.lang || '').toLowerCase(); return l.startsWith('zh'); } function sfGameNameCacheLoad() { if (_sfGameNameCache !== null) return _sfGameNameCache; _sfGameNameCache = {}; try { const raw = GM_getValue(SF_GAME_NAME_KEY); if (raw && typeof raw === 'object') _sfGameNameCache = raw; } catch (e) {} return _sfGameNameCache; } function sfGameNameCacheSave() { try { GM_setValue(SF_GAME_NAME_KEY, _sfGameNameCache || {}); } catch (e) {} } function fetchGameZhName(appid) { if (!isZhLocale()) return Promise.resolve(''); const id = String(appid); const cache = sfGameNameCacheLoad(); const cached = cache[id]; if (cached && cached.name && Date.now() - (cached.ts || 0) < SF_NAME_CACHE_TTL) { return Promise.resolve(cached.name); } // 如果正在批量加载中,等待批量完成后再查缓存,避免重复单条请求 if (_batchNameLock.has(id)) { return new Promise(resolve => { const check = (retries = 20) => { const c = sfGameNameCacheLoad()[id]; if (c && c.name) { resolve(c.name); return; } if (!_batchNameLock.has(id) || retries <= 0) { // 批量结束仍未命中,回退到单条请求 _fetchGameZhNameSingle(id).then(resolve); } else { setTimeout(() => check(retries - 1), 300); } }; check(); }); } if (_sfGameNamePending.has(id)) return _sfGameNamePending.get(id); return _fetchGameZhNameSingle(id); } // 单条 appdetails 请求(兜底) function _fetchGameZhNameSingle(id) { if (_sfGameNamePending.has(id)) return _sfGameNamePending.get(id); const cache = sfGameNameCacheLoad(); const p = new Promise((resolve) => { const doReq = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : null; if (!doReq) { resolve(''); return; } doReq({ method: 'GET', url: `https://store.steampowered.com/api/appdetails?appids=${id}&l=schinese&filters=basic`, timeout: 15000, onload: (resp) => { let name = ''; try { const d = JSON.parse(resp.responseText); if (d && d[id] && d[id].success && d[id].data && d[id].data.name) name = d[id].data.name; } catch (e) {} if (name) { cache[id] = { name, ts: Date.now() }; sfGameNameCacheSave(); } resolve(name); }, onerror: () => resolve(''), ontimeout: () => resolve(''), }); }); _sfGameNamePending.set(id, p); p.finally(() => { _sfGameNamePending.delete(id); }); return p; } // 异步加载中文名并更新 DOM 元素 function loadGameZhName(el, appid, originalName) { if (!el || !appid) return; fetchGameZhName(appid).then(zhName => { if (zhName && zhName !== originalName) { el.textContent = zhName; el.title = `${zhName} (${originalName})`; // 清除占位名(App xxxxxx / 空)遗留的内联样式,让 CSS 类样式生效 el.style.color = ''; el.style.fontStyle = ''; el.style.fontWeight = ''; } }); } // ==================== 批量中文名预加载(参考 lib core IStoreBrowseService/GetItems) ==================== // 100 个/批、3 路并发,匿名可用,几秒完成数百条,替代绝大多数 appdetails 单条请求 const _batchNameLock = new Set(); // 防止同一批 appid 重复请求 function getCountryCode() { try { const win = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; return win.g_strCountryCode || 'CN'; } catch { return 'CN'; } } async function batchPreloadZhNames(appids) { if (!isZhLocale() || !appids || appids.length === 0) return; const cache = sfGameNameCacheLoad(); // 过滤出需要请求的 appid(缓存未命中或过期) const need = []; const seen = new Set(); for (const id of appids) { const sid = String(id); if (seen.has(sid)) continue; seen.add(sid); const c = cache[sid]; if (!c || !c.name || Date.now() - (c.ts || 0) >= SF_NAME_CACHE_TTL) { if (!_batchNameLock.has(sid)) need.push(sid); } } if (need.length === 0) return; // 标记锁定 need.forEach(id => _batchNameLock.add(id)); const CHUNK = 100, CONCURRENCY = 3; const chunks = []; for (let i = 0; i < need.length; i += CHUNK) chunks.push(need.slice(i, i + CHUNK)); const cc = getCountryCode(); const lang = 'schinese'; let qIdx = 0; const worker = async () => { while (qIdx < chunks.length) { const chunk = chunks[qIdx++]; try { const input = { ids: chunk.map(id => ({ appid: Number(id) })), context: { language: lang, country_code: cc, steam_realm: 1 }, data_request: { include_release: true, include_all_purchase_options: false, include_platforms: false, include_assets: true } }; const url = 'https://api.steampowered.com/IStoreBrowseService/GetItems/v1?input_json=' + encodeURIComponent(JSON.stringify(input)); const doReq = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : null; if (!doReq) { await new Promise(r => setTimeout(r, 50)); continue; } const respText = await new Promise((resolve) => { doReq({ method: 'GET', url, timeout: 20000, onload: (r) => resolve(r.responseText || ''), onerror: () => resolve(''), ontimeout: () => resolve(''), }); }); if (respText) { const resp = JSON.parse(respText); const storeItems = resp?.response?.store_items || []; for (const si of storeItems) { if (si && si.appid) { const sid = String(si.appid); // 缓存中文名 if (si.name) { cache[sid] = { name: String(si.name), ts: Date.now() }; } // 预缓存封面 URL(GetItems assets 返回完整 URL) const assets = si.assets; if (assets) { const coverUrl = assets.header_image || assets.main_capsule || assets.small_capsule || ''; if (coverUrl) { _capsuleCache.set(sid, coverUrl); recordCoverGood(sid, coverUrl); } } } } } } catch (e) { /* 忽略单批失败 */ } await new Promise(r => setTimeout(r, 200)); } }; const workers = []; for (let i = 0; i < CONCURRENCY; i++) workers.push(worker()); await Promise.all(workers); // 解锁并持久化缓存 need.forEach(id => _batchNameLock.delete(id)); try { sfGameNameCacheSave(); } catch (e) {} } // 批量异步加载容器内所有 [data-sf-appid] 元素的中文名 // 先用 GetItems 批量预加载缓存,再逐个从缓存读取更新 DOM async function loadGameZhNamesInContainer(container) { if (!container || !isZhLocale()) return; const els = container.querySelectorAll('[data-sf-appid]'); if (els.length === 0) return; // 收集所有 appid const appids = []; els.forEach(el => { const id = el.getAttribute('data-sf-appid'); if (id) appids.push(id); }); // 批量预加载 await batchPreloadZhNames(appids); // 逐个从缓存读取更新 DOM(缓存未命中的回退到单条 appdetails) els.forEach(el => { const id = el.getAttribute('data-sf-appid'); if (!id) return; const cache = sfGameNameCacheLoad(); const c = cache[String(id)]; if (c && c.name) { const originalName = el.textContent; if (c.name !== originalName) { el.textContent = c.name; el.title = `${c.name} (${originalName})`; el.style.color = ''; el.style.fontStyle = ''; el.style.fontWeight = ''; } } else { // 缓存未命中,回退到单条请求 loadGameZhName(el, id, el.textContent); } }); } // ==================== 绝版游戏 DB 加载(单次下载,本地缓存)==================== // 优先 GM_getResourceText(@resource 预下载,无 CORS 问题),其次 GM_xmlhttpRequest,最后 fetch async function loadDelistedDB() { if (state.delistedDB) return state.delistedDB; const cacheKey = 'delisted_db'; const cached = cache.get(cacheKey); if (cached) { state.delistedDB = cached; return cached; } try { let jsonText = ''; // 1. 优先使用 @resource 预下载的数据(无 CORS 问题) if (typeof GM_getResourceText === 'function') { try { jsonText = GM_getResourceText('delistedData') || ''; } catch (e) { jsonText = ''; } } // 2. 回退到 GM_xmlhttpRequest(绕过 CORS) if (!jsonText && typeof GM_xmlhttpRequest !== 'undefined') { jsonText = await new Promise((resolve, reject) => { const controller = setTimeout(() => reject(new Error('GM_xhr timeout')), 30000); GM_xmlhttpRequest({ method: 'GET', url: CONFIG.DELISTED_DB_URL, onload: (resp) => { clearTimeout(controller); resolve(resp.responseText || ''); }, onerror: () => { clearTimeout(controller); reject(new Error('GM_xhr error')); }, ontimeout: () => { clearTimeout(controller); reject(new Error('GM_xhr timeout')); }, }); }); } // 3. 最后回退到 fetch(可能受 CORS 限制) if (!jsonText) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); const resp = await fetch(CONFIG.DELISTED_DB_URL, { signal: controller.signal }); clearTimeout(timeout); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); jsonText = await resp.text(); } const data = JSON.parse(jsonText); state.delistedDB = data; cache.set(cacheKey, data); console.log('[SF] 绝版游戏 DB 加载成功,条目数', (data.data || data.delisted?.apps || []).length); return data; } catch (e) { console.warn('[SF] 绝版游戏 DB 加载失败:', e); state.delistedDB = { data: [] }; cache.set(cacheKey, state.delistedDB); return state.delistedDB; } } function getDelistedApps() { const db = state.delistedDB; if (!db) return []; let rawApps = []; if (Array.isArray(db.data)) rawApps = db.data; else if (db.delisted && Array.isArray(db.delisted.apps)) rawApps = db.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 || '-'), // achievementCount: -1 表示无数据,0 表示无成就;仅保留正数,避免误显 "🏆 -1" achievements: (typeof app.achievementCount === 'number' && app.achievementCount > 0) ? app.achievementCount : (app.achievements || 0), keyshopPrice: app.keyshopPrice, source: app.source, })); } function getDelistedTypeKey(type) { if (!type) return 'delisted'; if (typeof type === 'string' && 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 getDelistedTypeLabel(type) { if (!type) return '绝版'; const map = { 'delisted': '绝版', 'purchase_disabled': '停售', 'f2p_unavailable': 'F2P下架', 'retail_only': '仅实体', 'test_app': '测试', }; return map[type] || type; } // ==================== 年度大作游戏 DB 加载(单次下载,本地缓存)==================== // 数据源:[{name, date, appid}],date 格式如 "2026年1月22日" async function loadAnnualAaaDB() { if (state.annualAaaDB) return state.annualAaaDB; const cacheKey = 'annual_aaa_db'; const cached = cache.get(cacheKey); if (cached) { state.annualAaaDB = cached; return cached; } try { let jsonText = ''; // 1. 优先使用 @resource 预下载的数据 if (typeof GM_getResourceText === 'function') { try { jsonText = GM_getResourceText('annualAaaData') || ''; } catch (e) { jsonText = ''; } } // 2. 回退到 GM_xmlhttpRequest if (!jsonText && typeof GM_xmlhttpRequest !== 'undefined') { jsonText = await new Promise((resolve, reject) => { const controller = setTimeout(() => reject(new Error('GM_xhr timeout')), 30000); GM_xmlhttpRequest({ method: 'GET', url: CONFIG.ANNUAL_AAA_URL, onload: (resp) => { clearTimeout(controller); resolve(resp.responseText || ''); }, onerror: () => { clearTimeout(controller); reject(new Error('GM_xhr error')); }, ontimeout: () => { clearTimeout(controller); reject(new Error('GM_xhr timeout')); }, }); }); } // 3. 最后回退到 fetch if (!jsonText) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); try { const resp = await fetch(CONFIG.ANNUAL_AAA_URL, { signal: controller.signal, credentials: 'omit' }); jsonText = await resp.text(); } finally { clearTimeout(timeout); } } const data = JSON.parse(jsonText); state.annualAaaDB = data; cache.set(cacheKey, data, 24 * 3600 * 1000); // 缓存 24 小时 console.log('[SF] 年度大作 DB 加载成功,条目数', data.length); return data; } catch (e) { console.warn('[SF] 年度大作 DB 加载失败:', e); state.annualAaaDB = []; return []; } } // 从 date 字符串(如 "2026年1月22日")提取年份 function parseYearFromDate(dateStr) { if (!dateStr) return null; const m = String(dateStr).match(/(\d{4})年/); return m ? parseInt(m[1], 10) : null; } // 获取按年分组的年度大作列表 function getAnnualAaaGames() { const raw = state.annualAaaDB; if (!raw || !Array.isArray(raw)) return []; return raw.map(item => ({ appid: item.appid, name: item.name || `App ${item.appid}`, date: item.date || '', year: parseYearFromDate(item.date), })); } // 检测用户拥有的年度大作 async function fetchAnnualAaaFor(steamId, ownedData) { if (!steamId) return { games: [], ownedSet: new Set(), total: 0, byYear: {} }; if (!ownedData || !ownedData.games?.length) return { games: [], ownedSet: new Set(), total: 0, byYear: {} }; await loadAnnualAaaDB(); const allAaa = getAnnualAaaGames(); if (!allAaa.length) return { games: [], ownedSet: new Set(), total: 0, byYear: {} }; const myAppIds = new Set(ownedData.games.map(g => Number(g.appid))); const gameMap = new Map(ownedData.games.map(g => [Number(g.appid), g])); const games = allAaa.map(item => { const owned = myAppIds.has(Number(item.appid)); const gameInfo = owned ? gameMap.get(Number(item.appid)) || {} : {}; return { ...item, owned, playtime_forever: gameInfo.playtime_forever || 0, rtime_last_played: gameInfo.rtime_last_played || 0, }; }); // 按年分组 const byYear = {}; for (const g of games) { const y = g.year || '未知'; if (!byYear[y]) byYear[y] = []; byYear[y].push(g); } // 每年内按日期排序 for (const y of Object.keys(byYear)) { byYear[y].sort((a, b) => (a.date || '').localeCompare(b.date || '', 'zh-CN')); } const ownedCount = games.filter(g => g.owned).length; console.log('[SF] 年度大作检测:总大作', games.length, '已拥有', ownedCount); return { games, ownedSet: new Set(games.filter(g => g.owned).map(g => g.appid)), total: games.length, byYear, ownedCount }; } // ==================== 通用 miniprofile 抓取(含等级、游戏数、地区、好友时长、状态)==================== // Steam miniprofile ID = steamId - 76561197960265728 function steamIdToMiniId(steamId) { if (!/^\d{17}$/.test(steamId)) return ''; return String(Number(steamId) - 76561197960265728); } async function fetchMiniprofile(steamId) { if (!steamId) return null; const cacheKey = `mini_${steamId}`; const cached = cache.get(cacheKey); if (cached) return cached; try { const miniId = steamIdToMiniId(steamId); if (!miniId) return null; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT); const resp = await fetch(`https://steamcommunity.com/miniprofile/${miniId}`, { signal: controller.signal, credentials: 'include' }); clearTimeout(timeout); if (!resp.ok) return null; const html = await resp.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); // 等级(miniprofile 使用 friendPlayerLevelNum) const lvlEl = doc.querySelector('.friendPlayerLevelNum, .friend_small_level_num, .friend_player_level_num'); const level = lvlEl ? parseInt(lvlEl.textContent, 10) : null; // 在线状态 let status = 'offline'; if (doc.querySelector('.in-game, .miniprofile_ingame')) status = 'in-game'; else if (doc.querySelector('.online') && !doc.querySelector('.offline')) status = 'online'; // 当前游戏 const gameName = doc.querySelector('.miniprofile_ingame_name, .in-game-name')?.textContent?.trim() || ''; // 国家(miniprofile 通常不显示国旗,资料页才有) const flag = doc.querySelector('img[src*="flags/"]'); let countryCode = ''; let countryName = ''; if (flag) { const m = (flag.getAttribute('src') || '').match(/flags\/([a-z]{2,3})/i); if (m) countryCode = m[1].toUpperCase().slice(0, 2); countryName = flag.getAttribute('title') || flag.getAttribute('alt') || ''; } // 好友时长 const sinceEl = doc.querySelector('.friendPlayerSince, .friend_since')?.textContent?.trim() || ''; // 头像框 const frame = doc.querySelector('.playersection_avatar_frame img, .profile_avatar_frame img')?.getAttribute('src') || ''; // 游戏库数量 const gameLink = doc.querySelector('a[href*="/games?tab=all"]'); let gameCount = null; if (gameLink) { const match = gameLink.textContent.match(/(\d+)/); gameCount = match ? parseInt(match[1], 10) : null; } const info = { level, status, gameName, countryCode, countryName, since: sinceEl, frame, gameCount }; cache.set(cacheKey, info); return info; } catch (e) { console.warn('[SF] miniprofile 抓取失败:', e); return null; } } // ==================== 个人游戏库 scraping(无 API Key 也可用)==================== async function fetchGamesPage(steamId, tab = 'all') { if (!steamId) return { games: [], total: 0 }; const url = `https://steamcommunity.com/profiles/${steamId}/games/?tab=${tab}`; try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT); const resp = await fetch(url, { signal: controller.signal, credentials: 'omit' }); clearTimeout(timeout); if (!resp.ok) return { games: [], total: 0 }; const html = await resp.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); return parseGamesFromHtml(doc, tab); } catch (e) { console.warn('[SF] games 页抓取失败:', e); return { games: [], total: 0 }; } } function parseGamesFromHtml(doc, tab = 'all') { const rows = doc.querySelectorAll('.gameListRow'); const games = []; rows.forEach(row => { const appid = parseInt(row.id?.replace('game_', '') || row.dataset?.appid || '0', 10); if (!appid) return; const name = row.querySelector('.gameListRowItemName, .gameListRowItem h2, .gameListRowItem .gameListRowItemName')?.textContent?.trim() || row.querySelector('a[href*="/app/"]')?.textContent?.trim() || ''; const logoImg = row.querySelector('img.gameListLogo, img[src*="capsule"], img[src*="header"]'); const logoSrc = logoImg?.getAttribute('src') || ''; const playtimeEl = row.querySelector('.gameListRowItem .gameListRowItemHours, .hours_played, .gameListRowItem .gameListRowItemTime, .gameListRowItem .gameListRowItemStat'); const playtimeText = playtimeEl?.textContent?.trim() || ''; const playtime = parsePlaytimeTextToMinutes(playtimeText); const lastPlayedText = row.querySelector('.gameListRowItem .gameListRowItemLastPlayed, .last_played, .gameListRowItem .gameListRowItemTime')?.textContent?.trim() || ''; const lastPlayed = parseLastPlayedToTs(lastPlayedText); games.push({ appid, name: name || `App ${appid}`, playtime_forever: playtime, playtime_2weeks: 0, rtime_last_played: lastPlayed, img_icon_url: '', logoSrc }); }); // 数字总数 const totalEl = doc.querySelector('.gameListRowItem .gameListRowItemCount, .gameListRow .gameListRowItem .gameListRowItemCount'); const totalText = totalEl?.textContent || ''; const totalMatch = totalText.match(/(\d+)/); const total = totalMatch ? parseInt(totalMatch[1], 10) : games.length; return { games, total }; } function parsePlaytimeTextToMinutes(text) { if (!text) return 0; // "1.2 hrs" / "2小时30分钟" / "30 minutes" const t = text.trim(); const m1 = t.match(/([\d.]+)\s*hrs?/i); if (m1) return Math.round(parseFloat(m1[1]) * 60); const m2 = t.match(/([\d.]+)\s*小时/); if (m2) { const h = parseFloat(m2[1]); const minM = t.match(/(\d+)\s*分钟/); const min = minM ? parseInt(minM[1], 10) : 0; return Math.round(h * 60 + min); } const m3 = t.match(/(\d+)\s*minutes?/i); if (m3) return parseInt(m3[1], 10); return 0; } function parseLastPlayedToTs(text) { if (!text) return 0; const t = text.trim(); if (!/last|最后|上次|play|今天|昨天|前天|\d{4}年|days?|hours?/i.test(t)) return 0; // 简单估算:"今天"=现在,"昨天"=1天前,"2 天前"等 const now = Math.floor(Date.now() / 1000); if (/今天|just now|几分钟前|a few minutes ago/i.test(t)) return now; if (/昨天|yesterday/i.test(t)) return now - 86400; if (/前天/i.test(t)) return now - 2 * 86400; const dayM = t.match(/(\d+)\s*天前|(\d+)\s*days?\s*ago/i); if (dayM) return now - parseInt(dayM[1] || dayM[2], 10) * 86400; // 默认按最近处理,避免缺失 return now; } function formatPlaytimeShort(min) { if (!min || min === 0) return '0h'; const h = Math.floor(min / 60); if (h < 1) return `${min}m`; return `${h}h`; } function formatLastPlayedShort(ts) { if (!ts) return ''; const diff = Date.now() / 1000 - ts; if (diff < 0) return ''; if (diff < 86400) return '今天'; if (diff < 604800) return `${Math.floor(diff / 86400)}天前`; if (diff < 2592000) return `${Math.floor(diff / 604800)}周前`; if (diff < 31536000) return `${Math.floor(diff / 2592000)}个月前`; return `${Math.floor(diff / 31536000)}年前`; } // ==================== 获取拥有的游戏:优先 API Key,否则 fallback 抓取页面 ==================== async function fetchOwnedGamesApi(steamId) { if (!steamId) return null; const cacheKey = `owned_api_${steamId}`; const cached = cache.get(cacheKey); if (cached) return cached; const apiKey = getApiKey(); // 1. 优先使用 API Key if (apiKey) { try { const data = await steamApi('/IPlayerService/GetOwnedGames/v0001/', { key: apiKey, steamid: steamId, include_appinfo: 1, include_played_free_games: 1, format: 'json', }); const games = (data.response?.games || []).map(g => ({ appid: g.appid, name: g.name || `App ${g.appid}`, playtime_forever: g.playtime_forever || 0, playtime_2weeks: g.playtime_2weeks || 0, img_icon_url: g.img_icon_url || '', rtime_last_played: g.rtime_last_played || 0, })); const result = { games, total: data.response?.game_count || games.length }; cache.set(cacheKey, result); return result; } catch (e) { console.warn('[SF] GetOwnedGames 失败,尝试页面抓取:', e); } } // 2. 无 API Key 或 API 失败时,抓取个人游戏库页面 try { const result = await fetchGamesPage(steamId); cache.set(cacheKey, result); return result; } catch (e) { console.warn('[SF] 抓取游戏库页面失败:', e); return null; } } async function fetchRecentlyPlayedApi(steamId) { const apiKey = getApiKey(); if (!apiKey || !steamId) return null; const cacheKey = `recent_api_${steamId}`; const cached = cache.get(cacheKey); if (cached) return cached; try { const data = await steamApi('/IPlayerService/GetRecentlyPlayedGames/v0001/', { key: apiKey, steamid: steamId, format: 'json', count: 20, }); const games = (data.response?.games || []).map(g => ({ appid: g.appid, name: g.name || `App ${g.appid}`, playtime_forever: g.playtime_forever || 0, playtime_2weeks: g.playtime_2weeks || 0, img_icon_url: g.img_icon_url || '', })); cache.set(cacheKey, games); return games; } catch (e) { console.warn('[SF] GetRecentlyPlayedGames 失败:', e); return null; } } async function fetchPlayerBansApi(steamIds) { const apiKey = getApiKey(); if (!apiKey || !steamIds?.length) return null; // 限制单次最多 100(API 上限) const ids = steamIds.slice(0, CONFIG.STEAMAPI_BATCH); try { const data = await steamApi('/ISteamUser/GetPlayerBans/v1/', { key: apiKey, steamids: ids.join(','), }); return (data.players || []).reduce((m, p) => { m[p.SteamId] = { vac: p.VACBanned || false, gameBan: p.GameBanned || false, vacCount: p.NumberOfVACBans || 0, gameBanCount: p.NumberOfGameBans || 0, daysSinceLastBan: p.DaysSinceLastBan || 0, communityBanned: p.CommunityBanned || false, tradeBan: p.EconomyBan || 'none', }; return m; }, {}); } catch (e) { console.warn('[SF] GetPlayerBans 失败:', e); return null; } } async function fetchPlayerSummariesApi(steamIds) { const apiKey = getApiKey(); if (!apiKey || !steamIds?.length) return null; const ids = steamIds.slice(0, CONFIG.STEAMAPI_BATCH); try { const data = await steamApi('/ISteamUser/GetPlayerSummaries/v2/', { key: apiKey, steamids: ids.join(','), }); return (data.response?.players || []).reduce((m, p) => { m[p.steamid] = { name: p.personaname || '', avatar: p.avatarfull || p.avatarmedium || '', profileUrl: p.profileurl || '', countryCode: p.loccountrycode || '', gameName: p.gameextrainfo || '', gameId: p.gameid || '', status: p.personastate || 0, // 0=offline, 1=online, 2=busy, 3=away, 4=snooze, 5=looking to trade, 6=looking to play lastLogoff: p.lastlogoff || 0, timeCreated: p.timecreated || 0, realName: p.realname || '', city: p.loccityid || '', }; return m; }, {}); } catch (e) { console.warn('[SF] GetPlayerSummaries 失败:', e); return null; } } // ==================== 获取 Steam 等级(GetSteamLevel API,参考 friend-manager 1.0.16)==================== async function fetchSteamLevelApi(steamId) { const apiKey = getApiKey(); if (!apiKey || !steamId) return null; const cacheKey = `level_${steamId}`; const cached = cache.get(cacheKey); if (cached != null) return cached; try { const data = await steamApi('/IPlayerService/GetSteamLevel/v1/', { key: apiKey, steamid: steamId, }); const level = data?.response?.player_level || 0; cache.set(cacheKey, level); return level; } catch (e) { console.warn('[SF] GetSteamLevel 失败:', e); return null; } } // ==================== 综合获取等级:优先 API,回退 miniprofile ==================== async function fetchLevel(steamId) { if (!steamId) return null; // 1. 优先 API(有 key 时最可靠) if (getApiKey()) { const apiLevel = await fetchSteamLevelApi(steamId); if (apiLevel != null && apiLevel > 0) return apiLevel; } // 2. 回退到 miniprofile scraping const mini = await fetchMiniprofile(steamId); if (mini && mini.level != null) return mini.level; return null; } // ==================== 更新好友等级 DOM 显示 ==================== function updateFriendLevelDisplay(steamId, level) { if (level == null || isNaN(level) || level <= 0) return; const host = document.querySelector(`.sf-fp-level-host[data-sfid="${steamId}"]`); if (host) { host.style.display = ''; host.innerHTML = `${level}`; } } // ==================== 获取好友列表(参考 friend-manager 1.0.16)==================== async function fetchFriendListApi(steamId) { const apiKey = getApiKey(); if (!apiKey || !steamId) return null; const cacheKey = `friendlist_api_${steamId}`; const cached = cache.get(cacheKey); if (cached) return cached; try { const data = await steamApi('/ISteamUser/GetFriendList/v0001/', { key: apiKey, steamid: steamId, relationship: 'friend', format: 'json', }); const friends = data.friendslist?.friends || []; // 批量获取摘要(含自己) const idsToFetch = [steamId, ...friends.map(f => f.steamid)]; const summaries = {}; const BATCH = CONFIG.STEAMAPI_BATCH; for (let i = 0; i < idsToFetch.length; i += BATCH) { const batch = idsToFetch.slice(i, i + BATCH); const sum = await fetchPlayerSummariesApi(batch); if (sum) Object.assign(summaries, sum); } const ownSummary = summaries[steamId] || {}; const enriched = friends.map(f => { const s = summaries[f.steamid] || {}; return { steamid: f.steamid, friend_since: f.friend_since || 0, personaname: s.name || f.steamid, avatar: s.avatar || 'https://avatars.steamstatic.com/fef49e7fa7e1997310dd48961da2e7d95a5c7a56_medium.jpg', profileurl: s.profileUrl || `https://steamcommunity.com/profiles/${f.steamid}/`, countryCode: s.countryCode || '', personastate: s.status !== undefined ? s.status : 0, lastlogoff: s.lastLogoff || 0, gameextrainfo: s.gameName || '', }; }); const result = { own: { steamid: steamId, personaname: ownSummary.name || '', avatar: ownSummary.avatar || '', profileurl: ownSummary.profileUrl || '', countryCode: ownSummary.countryCode || '', personastate: ownSummary.status || 0, gameextrainfo: ownSummary.gameName || '' }, friends: enriched, }; cache.set(cacheKey, result); return result; } catch (e) { console.warn('[SF] GetFriendList 失败:', e); return null; } } // ==================== 绝版游戏检测(与个人游戏库交叉对比)==================== async function fetchDelistedFor(steamId, ownedData) { if (!steamId) return { owned: [], total: 0 }; if (!ownedData || !ownedData.games?.length) return { owned: [], total: 0 }; await loadDelistedDB(); const apps = getDelistedApps(); if (!apps.length) { console.warn('[SF] 绝版数据库为空,跳过检测'); return { owned: [], total: 0 }; } const myAppIds = new Set(ownedData.games.map(g => Number(g.appid))); console.log('[SF] 绝版检测:用户游戏数', myAppIds.size, '绝版库数量', apps.length); const owned = []; for (const app of apps) { if (!app.appid) continue; const aid = Number(app.appid); if (isNaN(aid)) continue; if (myAppIds.has(aid)) { const gameInfo = ownedData.games.find(g => Number(g.appid) === aid) || {}; owned.push({ appid: aid, name: gameInfo.name || app.name || `App ${aid}`, playtime_forever: gameInfo.playtime_forever || 0, rtime_last_played: gameInfo.rtime_last_played || 0, img_icon_url: gameInfo.img_icon_url || '', delistedType: getDelistedTypeKey(app.type), delistedDate: app.changed || '', ownersPct: app.owners || '', achievements: app.achievements || 0, }); } } owned.sort((a, b) => (b.playtime_forever || 0) - (a.playtime_forever || 0)); console.log('[SF] 绝版检测结果:命中', owned.length, '款'); return { owned, total: apps.length }; } // ==================== 动态类型识别(v4.0:移除 friend 分类)==================== function getBlockType(block) { if (state.blockTypes.has(block)) return state.blockTypes.get(block); let type = 'other'; if (block.querySelector('.blotter_screenshot')) type = 'screenshot'; else if (block.querySelector('.blotter_gamepurchase')) type = 'game'; else if (block.querySelector('.blotter_recommendation')) type = 'review'; else if (block.querySelector('.blotter_workshopitempublished') || block.querySelector('.dynamiclink_box')) type = 'workshop'; else if (block.querySelector('.blotter_favorite') || block.querySelector('.blotter_ugc_favorite')) type = 'favorite'; else if (block.querySelector('.blotter_wishlist') || block.querySelector('[data-featuretarget="wishlist"]')) type = 'wishlist'; // v4.0: 移除 friend 分类(好友相关动态会被上面的 screenshot/review 等更具体的分类吃掉) else if (block.querySelector('.blotter_achievement') || block.querySelector('.blotter_award')) type = 'achievement'; state.blockTypes.set(block, type); block.dataset.sfType = type; return type; } // ==================== 筛选栏(v4.0:移除好友项)==================== const FILTER_ITEMS = [ { type: 'all', icon: 'all', label: '全部' }, { type: 'screenshot', icon: 'screenshot', label: '截图' }, { type: 'game', icon: 'game', label: '游戏' }, { type: 'review', icon: 'review', label: '评测' }, { type: 'workshop', icon: 'workshop', label: '工坊' }, { type: 'favorite', icon: 'favorite', label: '收藏' }, { type: 'wishlist', icon: 'wishlist', label: '愿望单' }, { type: 'achievement', icon: 'achievement', label: '成就' }, ]; function createFilterBar() { const blotterPage = $('#blotter_page'); if (!blotterPage) return; const filterBar = el('div', { id: 'sf-filter-bar' }); const row = el('div', { className: 'sf-filter-row sf-compact', id: 'sf-filter-row' }); FILTER_ITEMS.forEach(item => { const btn = el('button', { className: `sf-filter-btn${item.type === 'all' ? ' active' : ''}`, dataset: { filter: item.type }, }, `${ICONS[item.icon]}${item.label}0`); row.appendChild(btn); }); const moreBtn = el('button', { className: 'sf-filter-more', id: 'sf-filter-more' }, `更多${ICONS.collapse}`); row.appendChild(moreBtn); filterBar.appendChild(row); const statusEntry = blotterPage.querySelector('.blotter_status_entry'); blotterPage.insertBefore(filterBar, statusEntry?.nextElementSibling || blotterPage.firstChild); row.addEventListener('click', e => { const btn = e.target.closest('.sf-filter-btn'); if (!btn) return; row.querySelectorAll('.sf-filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.currentFilter = btn.dataset.filter; applyFilter(); }); moreBtn.addEventListener('click', e => { e.stopPropagation(); const isExpanded = row.classList.contains('sf-expanded'); row.classList.toggle('sf-expanded', !isExpanded); row.classList.toggle('sf-compact', isExpanded); moreBtn.classList.toggle('sf-open', !isExpanded); moreBtn.innerHTML = isExpanded ? `更多${ICONS.collapse}` : `收起${ICONS.expand}`; }); } // ==================== 左侧分类导航 ==================== function createSidebarNav() { if ($('#sf-sidebar-nav')) return; const sidebar = el('div', { id: 'sf-sidebar-nav' }); sidebar.innerHTML = `
${ICONS.sidebar}分类导航
`; FILTER_ITEMS.forEach(item => { const navItem = el('div', { className: `sf-sidebar-item${item.type === state.currentFilter ? ' active' : ''}`, dataset: { filter: item.type }, }, `${ICONS[item.icon]}${item.label}0`); navItem.addEventListener('click', () => { sidebar.querySelectorAll('.sf-sidebar-item').forEach(n => n.classList.remove('active')); navItem.classList.add('active'); state.currentFilter = item.type; applyFilter(); syncFilterBarState(); }); sidebar.appendChild(navItem); }); document.body.appendChild(sidebar); const overlay = el('div', { className: 'sf-sidebar-overlay', id: 'sf-sidebar-overlay' }); overlay.addEventListener('click', () => toggleSidebarVisibility(false)); document.body.appendChild(overlay); } function toggleSidebarVisibility(show) { const sidebar = $('#sf-sidebar-nav'); const overlay = $('#sf-sidebar-overlay'); if (!sidebar) return; state.sidebarVisible = typeof show === 'boolean' ? show : !state.sidebarVisible; sidebar.classList.toggle('sf-sidebar-open', state.sidebarVisible); document.body.classList.toggle('sf-sidebar-visible', state.sidebarVisible); // 桌面端 sidebar 模式为持久布局,不显示遮罩;仅在移动端临时弹出时显示 const isPersistent = document.body.classList.contains('sf-sidebar-mode'); overlay?.classList.toggle('sf-show', state.sidebarVisible && !isPersistent); } // ==================== 布局模式切换 ==================== function createLayoutToggleButton() { if ($('#sf-layout-btn')) return; const btn = el('button', { id: 'sf-layout-btn' }); btn.innerHTML = `${state.layoutMode === 'sidebar' ? ICONS.topbar : ICONS.sidebar}${state.layoutMode === 'sidebar' ? '顶部筛选' : '侧边导航'}`; btn.title = state.layoutMode === 'sidebar' ? '切换为顶部筛选' : '切换为侧边导航'; document.body.appendChild(btn); btn.addEventListener('click', () => switchLayoutMode(state.layoutMode === 'topbar' ? 'sidebar' : 'topbar')); } function switchLayoutMode(mode) { state.layoutMode = mode; try { localStorage.setItem('sf-layout-mode', mode); } catch (e) { /* 隐私模式可能抛错 */ } const filterBar = $('#sf-filter-bar'); const sidebar = $('#sf-sidebar-nav'); const layoutBtn = $('#sf-layout-btn'); const isSidebar = mode === 'sidebar'; document.body.classList.toggle('sf-sidebar-mode', isSidebar); if (filterBar) filterBar.style.display = isSidebar ? 'none' : ''; if (isSidebar && !sidebar) createSidebarNav(); toggleSidebarVisibility(isSidebar); if (layoutBtn) { layoutBtn.innerHTML = isSidebar ? ICONS.topbar : ICONS.sidebar; layoutBtn.title = isSidebar ? '切换为顶部筛选' : '切换为侧边导航'; } } function syncFilterBarState() { const filterBar = $('#sf-filter-bar'); if (filterBar) { filterBar.querySelectorAll('.sf-filter-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.filter === state.currentFilter); }); } } // ==================== 应用筛选 ==================== function applyFilter() { let visibleCount = 0; state.totalBlocks.forEach(block => { const type = getBlockType(block); const shouldShow = state.currentFilter === 'all' || type === state.currentFilter; if (shouldShow && !block.classList.contains('sf-lazy-hidden')) { block.classList.remove('sf-hidden'); visibleCount++; } else { block.classList.add('sf-hidden'); } }); state.visibleCount = visibleCount; updateEmptyState(); const sidebar = $('#sf-sidebar-nav'); if (sidebar) { sidebar.querySelectorAll('.sf-sidebar-item').forEach(item => { item.classList.toggle('active', item.dataset.filter === state.currentFilter); }); } } // ==================== 更新计数(v4.0:移除 friend)==================== function updateCounts() { const counts = { all: 0, screenshot: 0, game: 0, review: 0, workshop: 0, favorite: 0, wishlist: 0, achievement: 0 }; state.totalBlocks.forEach(block => { const type = getBlockType(block); if (counts[type] !== undefined) counts[type]++; counts.all++; }); Object.keys(counts).forEach(type => { const e2 = $(`#sf-count-${type}`); if (e2) e2.textContent = counts[type]; const sidebarEl = $(`#sf-sidebar-count-${type}`); if (sidebarEl) sidebarEl.textContent = counts[type]; }); } // ==================== 空状态 ==================== function updateEmptyState() { const blotterContent = $('#blotter_content'); if (!blotterContent) return; blotterContent.querySelector('.sf-empty-state')?.remove(); if (state.visibleCount === 0 && state.totalBlocks.length > 0) { blotterContent.appendChild(el('div', { className: 'sf-empty-state' }, '
📭
该分类暂无动态
')); } } // ==================== 懒加载控制 ==================== function initLazyLoad() { const blocks = $$('.blotter_block'); state.totalBlocks = Array.from(blocks); blocks.forEach((block, index) => { if (index >= CONFIG.INITIAL_LOAD) { block.classList.add('sf-lazy-hidden'); block.style.display = 'none'; } }); addLoadMoreButton(); updateCounts(); applyFilter(); } function addLoadMoreButton() { const blotterContent = $('#blotter_content'); if (!blotterContent) return; const loadMoreDiv = el('div', { id: 'sf-load-more' }, ``); blotterContent.appendChild(loadMoreDiv); $('#sf-load-btn').addEventListener('click', loadMoreBlocks); } function loadMoreBlocks() { const hiddenBlocks = $$('.blotter_block.sf-lazy-hidden'); if (hiddenBlocks.length === 0) { const loadMoreDiv = $('#sf-load-more'); if (loadMoreDiv) loadMoreDiv.style.display = 'none'; return; } const toShow = Array.from(hiddenBlocks).slice(0, CONFIG.LOAD_MORE_STEP); toShow.forEach(block => { block.classList.remove('sf-lazy-hidden'); block.style.display = ''; if (!state.totalBlocks.includes(block)) state.totalBlocks.push(block); }); applyFilter(); updateCounts(); const remaining = $$('.blotter_block.sf-lazy-hidden').length; const loadMoreDiv = $('#sf-load-more'); if (remaining === 0) { if (loadMoreDiv) loadMoreDiv.style.display = 'none'; } else { const btn = $('#sf-load-btn'); if (btn) btn.textContent = `加载更多 (${Math.min(CONFIG.LOAD_MORE_STEP, remaining)}条)`; } } // ==================== 全屏瀑布流模式 ==================== function createWaterfallButton() { if ($('#sf-waterfall-btn')) return; const btn = el('button', { id: 'sf-waterfall-btn' }); btn.innerHTML = `${ICONS.waterfall}瀑布流`; btn.title = '瀑布流模式'; document.body.appendChild(btn); btn.addEventListener('click', () => { if (!state.wfOpen) openWaterfallView(); }); } function openWaterfallView() { state.wfOpen = true; if (state.layoutMode === 'sidebar') toggleSidebarVisibility(false); const allBlocks = Array.from($$('.blotter_block')); state.wfBlocks = allBlocks; state.wfLoadedCount = 0; state.wfAllLoaded = false; state.wfColumns = []; const overlay = el('div', { id: 'sf-wf-overlay' }); overlay.innerHTML = `
${ICONS.waterfall} 瀑布流动态 0 / ${allBlocks.length}
`; document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; wfCreateColumns(); requestAnimationFrame(() => overlay.classList.add('sf-wf-open')); wfAppendBatch(); bindWaterfallEvents(overlay); } function wfCreateColumns() { const grid = $('#sf-wf-grid'); if (!grid) return; grid.innerHTML = ''; state.wfColumns = []; const bodyWidth = window.innerWidth || document.documentElement.clientWidth; const minColWidth = 340; const gap = 16; const maxCols = Math.max(1, Math.min(4, Math.floor((bodyWidth - 48 - gap) / (minColWidth + gap)))); for (let i = 0; i < maxCols; i++) { const col = el('div', { className: 'sf-wf-col', dataset: { colIndex: String(i) } }); grid.appendChild(col); state.wfColumns.push(col); } } function wfAppendBatch() { if (state.wfColumns.length === 0) return; const start = state.wfLoadedCount; const end = Math.min(start + CONFIG.WF_BATCH_SIZE, state.wfBlocks.length); if (start >= state.wfBlocks.length) { state.wfAllLoaded = true; const loadMore = $('#sf-wf-loadmore'); if (loadMore) loadMore.style.display = 'none'; return; } for (let i = start; i < end; i++) { const block = state.wfBlocks[i]; if (!block._wfOrigParent) { block._wfOrigParent = block.parentElement; block._wfOrigNext = block.nextElementSibling; } block.classList.remove('sf-hidden', 'sf-lazy-hidden'); block.style.display = ''; findShortestCol(state.wfColumns).appendChild(block); } state.wfLoadedCount = end; const countEl = $('#sf-wf-count'); if (countEl) countEl.textContent = `${end} / ${state.wfBlocks.length}`; if (end >= state.wfBlocks.length) { state.wfAllLoaded = true; const loadMore = $('#sf-wf-loadmore'); if (loadMore) loadMore.style.display = 'none'; } setTimeout(() => { expandGameCoverImages($('#sf-wf-grid')); fixGamePurchaseImages(); }, 100); } function bindWaterfallEvents(overlay) { overlay.querySelector('#sf-wf-close-btn').addEventListener('click', closeWaterfallView); const escHandler = e => { if (e.key === 'Escape' && state.wfOpen) closeWaterfallView(); }; document.addEventListener('keydown', escHandler); registerCleanup(() => document.removeEventListener('keydown', escHandler)); const loadBtn = overlay.querySelector('#sf-wf-load-btn'); loadBtn.addEventListener('click', () => { loadBtn.textContent = '加载中...'; loadBtn.disabled = true; setTimeout(() => { wfAppendBatch(); loadBtn.textContent = '加载更多'; loadBtn.disabled = false; }, 100); }); overlay.addEventListener('scroll', () => { if (state.wfAllLoaded) return; const { scrollTop, scrollHeight, clientHeight } = overlay; if (scrollHeight - scrollTop - clientHeight < CONFIG.WF_LOAD_THRESHOLD) wfAppendBatch(); }, { passive: true }); const resizeHandler = debounce(() => { if (!state.wfOpen) return; const grid = $('#sf-wf-grid'); if (!grid) return; const cards = Array.from(grid.querySelectorAll('.blotter_block')); wfCreateColumns(); cards.forEach(card => findShortestCol(state.wfColumns).appendChild(card)); }, 200); window.addEventListener('resize', resizeHandler); registerCleanup(() => window.removeEventListener('resize', resizeHandler)); } function closeWaterfallView() { const overlay = $('#sf-wf-overlay'); if (!overlay) return; overlay.classList.remove('sf-wf-open'); setTimeout(() => { state.wfBlocks.forEach(block => { if (block._wfOrigParent) { const parent = block._wfOrigParent; const next = block._wfOrigNext; if (next && next.parentElement === parent) parent.insertBefore(block, next); else parent.appendChild(block); if (state.currentFilter !== 'all' && getBlockType(block) !== state.currentFilter) block.classList.add('sf-hidden'); delete block._wfOrigParent; delete block._wfOrigNext; } }); overlay.remove(); document.body.style.overflow = ''; state.wfOpen = false; state.wfBlocks = []; state.wfColumns = []; state.wfLoadedCount = 0; state.wfAllLoaded = false; if (state.layoutMode === 'sidebar') toggleSidebarVisibility(true); }, 350); } // ==================== 游戏封面展开优化 ==================== function expandGameCoverImages(container) { const scope = container || document; const isWaterfall = !!container?.closest('#sf-wf-overlay'); const maxShow = isWaterfall ? CONFIG.WF_MAX_SHOW : CONFIG.MAIN_MAX_SHOW; scope.querySelectorAll('.blotter_gamepurchase').forEach(block => { if (block.dataset.sfGameExpanded) return; block.dataset.sfGameExpanded = '1'; const content = block.querySelector('.blotter_gamepurchase_content'); const details = block.querySelector('.blotter_gamepurchase_details'); const plusBlock = content?.querySelector('.plus_block'); if (!content || !details) return; if (!isWaterfall && !plusBlock) return; const existingLogos = content.querySelectorAll('.blotter_gamepurchase_logo'); if (!isWaterfall && existingLogos.length >= maxShow) return; const links = details.querySelectorAll('a[href*="store.steampowered.com/app/"]'); const existingAppids = new Set(); existingLogos.forEach(l => { const a = l.getAttribute('data-appid'); if (a) existingAppids.add(a); }); let added = 0; links.forEach(link => { if (existingLogos.length + added >= maxShow) return; const href = link.getAttribute('href') || ''; const match = href.match(/\/app\/(\d+)/); if (!match) return; const appid = match[1]; if (existingAppids.has(appid)) return; const a = el('a', { href: link.href, className: 'blotter_gamepurchase_logo es_ea_checked', title: `Game #${appid}`, target: '_blank' }); a.dataset.appid = appid; a.rel = 'noopener'; const img = document.createElement('img'); img.dataset.appid = appid; img.loading = 'lazy'; img.alt = `Game #${appid}`; loadCapsuleWithFallback(img, appid); a.appendChild(img); if (plusBlock) content.insertBefore(a, plusBlock); else content.appendChild(a); added++; }); if (plusBlock && !isWaterfall) { const totalLinks = links.length; const totalShown = existingLogos.length + added; if (totalShown < totalLinks) plusBlock.textContent = `+${totalLinks - totalShown}`; else plusBlock.remove(); } }); } // ==================== 所有游戏链接新标签页打开 ==================== function setupLinkTarget() { const blotter = document.getElementById('blotter_content'); if (!blotter) return; const setBlank = links => links.forEach(a => { if (a.target !== '_blank' && (a.href?.includes('store.steampowered.com') || a.href?.includes('steamcommunity.com'))) { a.target = '_blank'; a.rel = 'noopener'; } }); setBlank(blotter.querySelectorAll('a')); const mo = new MutationObserver(mutations => { for (const m of mutations) for (const node of m.addedNodes) { if (node.nodeType !== 1) continue; if (node.tagName === 'A') setBlank([node]); if (node.querySelectorAll) setBlank(node.querySelectorAll('a')); } }); mo.observe(blotter, { childList: true, subtree: true }); registerCleanup(() => mo.disconnect()); } // ==================== 修复宽度 ==================== function fixWidth() { const e2 = document.getElementById('blotter_content'); if (e2?.style.width) e2.removeAttribute('style'); $$('.highlight_strip_scroll').forEach(s => { if (s.style.width) s.style.removeProperty('width'); }); } // ==================== 监听新内容(带 debounce)==================== function observeNewContent() { const handler = debounce(() => { const newBlocks = $$('.blotter_block:not([data-sf-type])'); if (newBlocks.length > 0) { newBlocks.forEach(block => { if (!state.totalBlocks.includes(block)) state.totalBlocks.push(block); }); updateCounts(); applyFilter(); expandGameCoverImages(); fixGamePurchaseImages(); } enhanceAuthorInteraction(); }, CONFIG.OBSERVER_DEBOUNCE); const observer = new MutationObserver(handler); const blotterContent = $('#blotter_content') || document.body; observer.observe(blotterContent, { childList: true, subtree: true }); registerCleanup(() => observer.disconnect()); } // ==================== 动态页面图片查看器 ==================== let activityOverlayCreated = false; function setupActivityImageViewer() { if (isImageViewerPage() || activityOverlayCreated) return; activityOverlayCreated = true; const overlay = el('div', { className: 'sf-overlay', id: 'sf-activity-overlay' }); overlay.innerHTML = `
Preview
100%
滚轮缩放 · 拖拽移动 · ESC关闭
`; document.body.appendChild(overlay); const imgEl = overlay.querySelector('#sf-act-img'); const counterEl = overlay.querySelector('#sf-act-counter'); const prevBtn = overlay.querySelector('#sf-act-prev'); const nextBtn = overlay.querySelector('#sf-act-next'); const openBtn = overlay.querySelector('#sf-act-open'); let shots = [], idx = 0, downloadUrl = ''; const loadedOriginals = new Set(); const interaction = bindOverlayInteraction(overlay, imgEl, { zoomLabelSel: '#sf-act-zoom', hintSel: '#sf-act-hint', zoomInSel: '#sf-act-zi', zoomOutSel: '#sf-act-zo', imgWrapSel: '#sf-act-img-wrap', maxZoom: 8, minZoom: 0.2 }); function getShotKey(shot) { return shot.m_id || shot.m_baseurl || shot.m_img || ''; } function getDisplaySrc(shot, multi) { if (loadedOriginals.has(getShotKey(shot))) { return shot.m_baseurl ? `${shot.m_baseurl}?imw=5000&imh=5000&letterbox=false` : getSteamOriginalImageUrl(shot.m_img); } if (!multi) return getSteamOriginalImageUrl(shot.m_img); return shot.m_img || ''; } function updateOpenBtn() { const shot = shots[idx]; if (!shot || !openBtn) return; if (loadedOriginals.has(getShotKey(shot))) openBtn.innerHTML = `${ICONS.fullscreen}已加载`; else openBtn.innerHTML = `${ICONS.fullscreen}原图`; } function navTo(i) { if (i < 0 || i >= shots.length) return; idx = i; const shot = shots[idx]; const multi = shots.length > 1; imgEl.src = getDisplaySrc(shot, multi); downloadUrl = shot.m_baseurl ? `${shot.m_baseurl}?imw=5000&imh=5000&letterbox=false` : (multi ? shot.m_img : getSteamOriginalImageUrl(shot.m_img)); interaction.resetView(); prevBtn.style.display = nextBtn.style.display = counterEl.style.display = multi ? '' : 'none'; if (multi) counterEl.textContent = `${idx + 1} / ${shots.length}`; if (openBtn) openBtn.style.display = multi ? '' : 'none'; updateOpenBtn(); } window.openActivityImageView = function(src, galleryId, shotId) { shots = []; idx = 0; if (galleryId && typeof g_BlotterGalleries !== 'undefined' && g_BlotterGalleries[galleryId]) { const g = g_BlotterGalleries[galleryId]; const keys = Object.keys(g.shots || {}); if (keys.length) { shots = keys.map(k => g.shots[k]); if (shotId) { const fi = shots.findIndex(s => s.m_id === shotId); if (fi >= 0) idx = fi; } } } if (!shots.length) shots = [{ m_img: src, m_baseurl: '', m_id: '' }]; navTo(idx); overlay.classList.add('sf-show'); document.body.style.overflow = 'hidden'; }; function close() { overlay.classList.remove('sf-show'); document.body.style.overflow = ''; imgEl.src = ''; downloadUrl = ''; shots = []; idx = 0; prevBtn.style.display = nextBtn.style.display = counterEl.style.display = 'none'; } overlay.querySelector('#sf-act-close').onclick = close; overlay.addEventListener('click', e => { if (e.target === overlay || e.target === overlay.querySelector('#sf-act-img-wrap')) close(); }); prevBtn.onclick = e => { e.stopPropagation(); navTo(idx - 1); }; nextBtn.onclick = e => { e.stopPropagation(); navTo(idx + 1); }; openBtn.onclick = e => { e.stopPropagation(); const shot = shots[idx]; if (!shot) return; const shotKey = getShotKey(shot); if (loadedOriginals.has(shotKey)) return; const url = downloadUrl || imgEl.src; if (!url) return; openBtn.style.opacity = '0.6'; openBtn.innerHTML = `${ICONS.fullscreen}加载中...`; const hdImg = new Image(); hdImg.onload = () => { loadedOriginals.add(shotKey); imgEl.src = url; openBtn.style.opacity = '1'; openBtn.innerHTML = `${ICONS.fullscreen}已加载`; }; hdImg.onerror = () => { openBtn.style.opacity = '1'; openBtn.innerHTML = `${ICONS.fullscreen}原图`; }; hdImg.src = url; }; overlay.querySelector('#sf-act-dl').onclick = e => { e.stopPropagation(); const a = el('a', { href: downloadUrl || imgEl.src }); a.download = `steam_screenshot_${Date.now()}.jpg`; a.target = '_blank'; a.rel = 'noopener'; document.body.appendChild(a); a.click(); a.remove(); }; const onKey = e => { if (!overlay.classList.contains('sf-show')) return; if (e.key === 'Escape') close(); else if (e.key === 'ArrowLeft') navTo(idx - 1); else if (e.key === 'ArrowRight') navTo(idx + 1); }; document.addEventListener('keydown', onKey); registerCleanup(() => document.removeEventListener('keydown', onKey)); document.addEventListener('click', e => { const link = e.target.closest('.blotter_screenshot_image a, .blotter_screenshot_gallery_image a, .blotter_screenshot a'); if (!link) return; if (link.closest('#sf-filter-bar, #sf-friend-panel, #sf-overlay, #sf-activity-overlay, #sf-sidebar-nav, #sf-fa-panel, #sf-lib-panel')) return; const clickedImg = link.querySelector('img') || e.target.closest('img'); if (!clickedImg) return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const src = clickedImg.srcset?.split(' ')[0] || clickedImg.src; if (!src) return; const screenshotBlock = clickedImg.closest('.blotter_screenshot'); const galleryId = screenshotBlock?.querySelector('.blotter_screenshot_image')?.id || screenshotBlock?.querySelector('.highlight_strip_scroll')?.dataset.galleryid || ''; let shotId = ''; if (galleryId && typeof g_BlotterGalleries !== 'undefined' && g_BlotterGalleries[galleryId]) { shotId = g_BlotterGalleries[galleryId].m_screenshotActive || ''; } if (!shotId && clickedImg.id) { const parts = clickedImg.id.split('_'); if (parts.length >= 2) shotId = parts[parts.length - 1]; } window.openActivityImageView(src, galleryId, shotId); }, true); } // ==================== 页面级浮层(filedetails)==================== const viewerState = { overlayOpen: false, allImages: [], currentIndex: 0, commentsExpanded: false }; function isImageViewerPage() { return /\/sharedfiles\/filedetails/i.test(window.location.pathname); } function isSteamUserImageUrl(url) { return /(?:images\.steamusercontent\.com|steamuserimages|steamusercontent|\/ugc\/|steamstatic|cloudflare\.steamstatic)/i.test(url); } function getSteamOriginalImageUrl(imageSrc) { if (!imageSrc) return ''; try { const url = new URL(imageSrc, window.location.href); if (!isSteamUserImageUrl(url.href)) return imageSrc; url.searchParams.set('imw', String(STEAM_ORIGINAL_IMAGE_SIZE)); url.searchParams.set('imh', String(STEAM_ORIGINAL_IMAGE_SIZE)); url.searchParams.set('ima', 'fit'); url.searchParams.set('impolicy', 'Letterbox'); url.searchParams.set('imcolor', '#000000'); url.searchParams.set('letterbox', 'false'); return url.href; } catch (err) { if (!isSteamUserImageUrl(imageSrc)) return imageSrc; const [base] = imageSrc.split('?'); return `${base}?imw=${STEAM_ORIGINAL_IMAGE_SIZE}&imh=${STEAM_ORIGINAL_IMAGE_SIZE}&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=false`; } } function getHighResImageUrl(img) { const sourceImg = img?.matches?.('img') ? img : img?.querySelector?.('img') || img; const src = sourceImg?.srcset?.split(' ')[0] || sourceImg?.src || ''; if (!src) return null; return getSteamOriginalImageUrl(src); } function getOriginalImageUrl(imageSrc) { if (!imageSrc) return ''; let url = getSteamOriginalImageUrl(imageSrc); if (isSteamUserImageUrl(url)) return url; if (url.includes('steamstatic.com') || url.includes('cloudflare.steamstatic')) { url = url.replace(/(\d+)x(\d+)/, 'max').replace(/_\d+\.jpg/, '.jpg').replace(/\?.*$/, ''); } return url; } function findPageImage() { return document.querySelector('.actualScreenshotImg') || document.querySelector('#screenshotImage') || document.querySelector('.workshopItemPreviewHolder img') || document.querySelector('#previewImageMain') || document.querySelector('img[src*="steamuserimages"]') || document.querySelector('img[src*="steamcloud"]'); } function findCommentArea() { return document.querySelector('.commentthread_area') || document.querySelector('.commentthread') || document.querySelector('#commentthread'); } function initImageViewerEnhancement() { if (!isImageViewerPage()) return; let done = false; const tryInit = () => { if (!done) { done = true; enhanceImageViewerPage(); } }; const sels = ['.actualScreenshotImg', '#screenshotImage', '.workshopItemPreviewHolder', '#sharedfiles_previewarea', '.workshopItemDetails', '.commentthread_area']; for (const s of sels) waitForElement(s, tryInit, 3000); setTimeout(tryInit, 2500); } function enhanceImageViewerPage() { const img = findPageImage(); const comments = findCommentArea(); const mainContent = document.querySelector('.maincontent') || document.querySelector('#maincontent') || document.querySelector('body'); mainContent.classList.add('sf-page-enhanced'); collectPageImages(); if (img) { img.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); openOverlay(getHighResImageUrl(img)); }); } if (comments) collapseCommentsEnhanced(comments); addOverlayKeyboard(); addImageHint(img); console.log('[SF] 图片浮层优化已启用', { img: !!img, comments: !!comments, images: viewerState.allImages.length }); } function collectPageImages() { const imgs = document.querySelectorAll('img[src*="steamuserimages"], img[src*="steamcloud"], .actualScreenshotImg, #screenshotImage'); viewerState.allImages = []; imgs.forEach(i => { const src = getHighResImageUrl(i); if (src && !viewerState.allImages.includes(src)) viewerState.allImages.push(src); }); const current = findPageImage(); if (current) { viewerState.currentIndex = viewerState.allImages.indexOf(getHighResImageUrl(current)); if (viewerState.currentIndex === -1) viewerState.currentIndex = 0; } } function addImageHint(img) { if (!img || !img.parentElement) return; const hint = el('div'); hint.style.cssText = 'text-align:center;padding:8px;color:#8f98a0;font-size:12px;cursor:pointer;'; hint.textContent = '💡 点击图片查看大图(支持左右键切换)'; hint.addEventListener('click', () => openOverlay(getHighResImageUrl(img))); img.parentElement.parentElement.appendChild(hint); } function bindOverlayInteraction(overlay, imgEl, opts = {}) { let zoom = 1, rotation = 0, offsetX = 0, offsetY = 0; let isDragging = false, startX = 0, startY = 0, startOffsetX = 0, startOffsetY = 0; const zoomLabel = overlay.querySelector(opts.zoomLabelSel || '.sf-ov-zoom'); const hintEl = overlay.querySelector(opts.hintSel || '.sf-ov-hint'); setTimeout(() => { if (hintEl) hintEl.classList.add('sf-show'); }, 300); setTimeout(() => { if (hintEl) hintEl.classList.remove('sf-show'); }, 3300); function updateTransform() { imgEl.style.transform = rotation ? `scale(${zoom}) translate(${offsetX}px, ${offsetY}px) rotate(${rotation}deg)` : `scale(${zoom}) translate(${offsetX}px, ${offsetY}px)`; if (zoomLabel) zoomLabel.textContent = `${Math.round(zoom * 100)}%`; imgEl.classList.toggle('sf-can-drag', zoom > 1); } const zoomIn = () => { zoom = Math.min(zoom + 0.5, opts.maxZoom || 8); updateTransform(); }; const zoomOut = () => { zoom = Math.max(zoom - 0.5, opts.minZoom || 0.2); updateTransform(); }; const rotate = () => { rotation = (rotation + 90) % 360; updateTransform(); }; overlay.querySelector(opts.zoomInSel)?.addEventListener('click', e => { e.stopPropagation(); zoomIn(); }); overlay.querySelector(opts.zoomOutSel)?.addEventListener('click', e => { e.stopPropagation(); zoomOut(); }); overlay.querySelector(opts.rotateSel)?.addEventListener('click', e => { e.stopPropagation(); rotate(); }); const imgWrap = overlay.querySelector(opts.imgWrapSel); if (imgWrap) { imgWrap.addEventListener('wheel', e => { e.preventDefault(); zoom = Math.min(opts.maxZoom || 8, Math.max(opts.minZoom || 0.2, zoom + (e.deltaY > 0 ? -0.15 : 0.15))); updateTransform(); }, { passive: false }); } const onMouseDown = e => { if (zoom <= 1) return; e.preventDefault(); isDragging = true; startX = e.clientX; startY = e.clientY; startOffsetX = offsetX; startOffsetY = offsetY; imgEl.classList.add('sf-dragging'); }; imgEl.addEventListener('mousedown', onMouseDown); const onMouseMove = e => { if (!isDragging) return; offsetX = startOffsetX + (e.clientX - startX) / zoom; offsetY = startOffsetY + (e.clientY - startY) / zoom; updateTransform(); }; document.addEventListener('mousemove', onMouseMove); const onMouseUp = () => { if (isDragging) { isDragging = false; imgEl.classList.remove('sf-dragging'); } }; document.addEventListener('mouseup', onMouseUp); registerCleanup(() => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }); return { getZoom: () => zoom, setZoom: z => { zoom = z; updateTransform(); }, getRotation: () => rotation, setRotation: r => { rotation = r; updateTransform(); }, resetView: () => { zoom = 1; rotation = 0; offsetX = 0; offsetY = 0; imgEl.style.transform = ''; if (zoomLabel) zoomLabel.textContent = '100%'; imgEl.classList.remove('sf-can-drag', 'sf-dragging'); }, updateTransform }; } function openOverlay(src) { if (viewerState.overlayOpen) return; document.getElementById('sf-overlay')?.remove(); viewerState.overlayOpen = true; src = getOriginalImageUrl(src); const idx = viewerState.allImages.indexOf(src); if (idx !== -1) viewerState.currentIndex = idx; const hasMultiple = viewerState.allImages.length > 1; const displaySrc = hasMultiple ? src : getSteamOriginalImageUrl(src); const overlay = el('div', { className: 'sf-overlay', id: 'sf-overlay' }); overlay.innerHTML = `
${hasMultiple ? `${viewerState.currentIndex + 1} / ${viewerState.allImages.length}` : ''}
Preview
${hasMultiple ? `
100%
` : ''}
${hasMultiple ? '滚轮缩放 · 拖拽移动 · ESC关闭' : 'ESC关闭'}
${hasMultiple ? ` ` : ''} ${hasMultiple ? `` : ''}
`; document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; requestAnimationFrame(() => overlay.classList.add('sf-show')); const ovImg = overlay.querySelector('#sf-overlay-img'); const interaction = bindOverlayInteraction(overlay, ovImg, { zoomLabelSel: '#sf-ov-zoom', hintSel: '#sf-ov-hint', zoomInSel: '#sf-ov-zoomin', zoomOutSel: '#sf-ov-zoomout', rotateSel: '#sf-ov-rotate', imgWrapSel: '#sf-overlay-img-wrap', maxZoom: 5, minZoom: 0.5 }); overlay.querySelector('#sf-overlay-close').addEventListener('click', closeOverlay); overlay.addEventListener('click', e => { if (e.target === overlay || e.target.id === 'sf-overlay-img-wrap') closeOverlay(); }); const nav = dir => { if (viewerState.allImages.length <= 1) return; viewerState.currentIndex = (viewerState.currentIndex + dir + viewerState.allImages.length) % viewerState.allImages.length; const nextSrc = getOriginalImageUrl(viewerState.allImages[viewerState.currentIndex]); viewerState.allImages[viewerState.currentIndex] = nextSrc; ovImg.src = nextSrc; interaction.resetView(); const counter = overlay.querySelector('#sf-overlay-counter'); if (counter) counter.textContent = `${viewerState.currentIndex + 1} / ${viewerState.allImages.length}`; }; overlay.querySelector('#sf-ov-prev')?.addEventListener('click', e => { e.stopPropagation(); nav(-1); }); overlay.querySelector('#sf-edge-prev')?.addEventListener('click', e => { e.stopPropagation(); nav(-1); }); overlay.querySelector('#sf-ov-next')?.addEventListener('click', e => { e.stopPropagation(); nav(1); }); overlay.querySelector('#sf-edge-next')?.addEventListener('click', e => { e.stopPropagation(); nav(1); }); overlay.querySelector('#sf-ov-download').addEventListener('click', async e => { e.stopPropagation(); await downloadImage(ovImg.src, 'steam-screenshot.jpg', e); }); } function closeOverlay() { const overlay = document.querySelector('#sf-overlay'); if (!overlay) return; overlay.classList.remove('sf-show'); setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; viewerState.overlayOpen = false; }, 300); } function addOverlayKeyboard() { const onKey = e => { if (!viewerState.overlayOpen) return; switch (e.key) { case 'Escape': closeOverlay(); break; case 'ArrowLeft': document.querySelector('#sf-ov-prev')?.click(); break; case 'ArrowRight': document.querySelector('#sf-ov-next')?.click(); break; case '+': case '=': e.preventDefault(); document.querySelector('#sf-ov-zoomin')?.click(); break; case '-': e.preventDefault(); document.querySelector('#sf-ov-zoomout')?.click(); break; case 'r': case 'R': document.querySelector('#sf-ov-rotate')?.click(); break; } }; document.addEventListener('keydown', onKey); registerCleanup(() => document.removeEventListener('keydown', onKey)); } function collapseCommentsEnhanced(commentArea) { const commentContainer = commentArea.parentElement; if (!commentContainer || commentContainer.querySelector('.sf-collapse-hint')) return; const hint = el('div', { className: 'sf-collapse-hint' }, `
${ICONS.comment}评论区域(点击展开)
${ICONS.expand}
`); commentContainer.insertBefore(hint, commentArea); if (!viewerState.commentsExpanded) { commentArea.classList.add('sf-comments-collapsed'); } else { commentArea.classList.add('sf-comments-expanded'); hint.classList.add('sf-expanded'); } hint.addEventListener('click', () => { viewerState.commentsExpanded = !viewerState.commentsExpanded; const expanded = viewerState.commentsExpanded; commentArea.classList.toggle('sf-comments-expanded', expanded); commentArea.classList.toggle('sf-comments-collapsed', !expanded); hint.classList.toggle('sf-expanded', expanded); hint.querySelector('span').textContent = expanded ? '(点击折叠)' : '(点击展开)'; }); } async function downloadImage(imageSrc, filename = 'steam-image.jpg', triggerEvent = null) { try { const btn = triggerEvent?.target?.closest('.sf-overlay-action'); const origText = btn?.innerHTML || ''; if (btn) { btn.style.opacity = '0.6'; btn.textContent = '下载中...'; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT); let response; try { response = await fetch(imageSrc, { mode: 'cors', credentials: 'omit', signal: controller.signal }); } finally { clearTimeout(timeout); } if (!response.ok) throw new Error(`HTTP ${response.status}`); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const link = el('a', { href: blobUrl, download: filename }); document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => URL.revokeObjectURL(blobUrl), 100); if (btn) { btn.style.opacity = '1'; btn.innerHTML = origText; } } catch (err) { console.error('[SF] 图片下载失败:', err); const link = el('a', { href: imageSrc, download: filename }); link.click(); } } function waitForElement(selector, callback, timeout = 15000) { const found = document.querySelector(selector); if (found) { callback(found); return; } const observer = new MutationObserver(() => { const el2 = document.querySelector(selector); if (el2) { observer.disconnect(); callback(el2); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => observer.disconnect(), timeout); } // ==================== 好友浮窗 Toast ==================== function showFriendToast(msg, ms = 1800) { let toast = document.querySelector('.sf-fp-toast'); if (toast) toast.remove(); toast = document.createElement('div'); toast.className = 'sf-fp-toast'; toast.textContent = msg; document.body.appendChild(toast); requestAnimationFrame(() => requestAnimationFrame(() => toast.classList.add('sfp-show'))); setTimeout(() => { toast.classList.remove('sfp-show'); setTimeout(() => toast.remove(), 300); }, ms); } // ==================== 工具:从好友节点抓取扩展信息 ==================== function buildAvatarHtml(avatar, frame, statusClass, size = 36) { return `
${frame ? `
` : ''}
`; } function buildCountryFlagHtml(countryCode, countryName) { if (!countryCode) return ''; const flag = flagSvg(countryCode); if (!flag) return ''; return `${flag}${esc(countryName || countryCode)}`; } function buildShieldHtml(banned) { // banned: { vac, gameBan, communityBanned, devban? } if (!banned) return ''; const vac = banned.vac || banned.vacCount > 0; const game = banned.gameBan || banned.gameBanCount > 0; const community = banned.communityBanned; const devBan = banned.devban; let type = 'clean'; let title = ''; if (vac && (game || community)) { type = 'banned-both'; title = `VAC封禁 ${banned.vacCount || 0} 次,游戏封禁 ${banned.gameBanCount || 0} 次`; } else if (vac) { type = 'banned'; title = `VAC封禁 ${banned.vacCount || 0} 次`; } else if (game) { type = 'banned'; title = `游戏封禁 ${banned.gameBanCount || 0} 次`; } else if (devBan) { type = 'devban'; title = '开发者封禁'; } if (type === 'clean') return ''; const icon = type === 'devban' ? '' : ''; return `${icon}`; } function buildDaysTagHtml(since) { if (!since) return ''; const isNew = /新|new|recent/i.test(since); return `⏱ ${esc(since)}`; } function buildGamesTagHtml(count) { if (!count && count !== 0) return ''; return `🎮 ${count}`; } function buildLevelTagHtml(level) { if (!level && level !== 0) return ''; return `★ ${level}`; } function formatDaysSince(timestamp) { if (!timestamp) return ''; const days = Math.max(0, Math.floor((Date.now() / 1000 - timestamp) / 86400)); if (days === 0) return '今天'; if (days < 30) return `${days}天前`; if (days < 365) return `${Math.floor(days / 30)}个月前`; return `${Math.floor(days / 365)}年前`; } function waitForElementPromise(selector, timeout = 10000) { return new Promise((resolve) => { const el = document.querySelector(selector); if (el) { resolve(el); return; } const observer = new MutationObserver(() => { const el2 = document.querySelector(selector); if (el2) { observer.disconnect(); resolve(el2); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); resolve(null); }, timeout); }); } // ==================== 通过 Steam API 渲染好友列表面板 ==================== function renderOwnCardApi(list, own) { const ownCard = el('div', { className: 'sf-fp-own' }); ownCard.innerHTML = `
${esc(own.personaname)}
${esc(own.personaname)}
- - - -
🆔 ${esc(own.steamid || '-')} ${own.gameextrainfo ? `🎮 ${esc(own.gameextrainfo)}` : ''}
`; ownCard.querySelectorAll('.sf-fp-own-act').forEach(btn => { btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const act = btn.dataset.act; if (act === 'chat') { if (typeof LaunchWebChat === 'function') LaunchWebChat(); else showFriendToast('聊天功能未就绪'); } else if (act === 'library') { openPersonalLibrary(own.steamid); } }); }); list.appendChild(ownCard); // 异步填充个人 hero 统计数据 (async () => { try { const [ownedData, summaries, ownLevel] = await Promise.all([ fetchOwnedGamesApi(own.steamid).catch(() => null), getApiKey() ? fetchPlayerSummariesApi([own.steamid]).catch(() => null) : Promise.resolve(null), fetchLevel(own.steamid).catch(() => null), ]); // 等级 if (ownLevel != null) { const levelEl = ownCard.querySelector('#sf-fp-own-level'); const levelNumEl = ownCard.querySelector('#sf-fp-own-level-num'); if (levelEl) levelEl.style.display = ''; if (levelNumEl) levelNumEl.textContent = String(ownLevel); } const summary = summaries?.[own.steamid] || {}; if (ownedData && ownedData.games) { const gamesEl = ownCard.querySelector('#sf-fp-own-games'); if (gamesEl) gamesEl.textContent = String(ownedData.total || ownedData.games.length); const totalMin = ownedData.games.reduce((s, g) => s + (g.playtime_forever || 0), 0); const ptEl = ownCard.querySelector('#sf-fp-own-playtime'); if (ptEl) ptEl.textContent = totalMin > 0 ? formatPlaytimeShort(totalMin) : '0h'; } if (summary.countryCode) { const countryEl = ownCard.querySelector('#sf-fp-own-country'); if (countryEl) countryEl.innerHTML = flagSvg(summary.countryCode); } } catch (e) { console.warn('[SF] own hero stats error:', e); } })(); } function renderApiFriendSection(list, cls, icon, title, friends, statusKey) { const sec = el('div', { className: `sf-fp-section-title ${cls}` }); sec.innerHTML = `${icon} ${title}${friends.length}`; list.appendChild(sec); const listWrap = el('div', { className: 'sf-fp-list' }); friends.forEach(f => { const steamId = f.steamid; const statusClass = statusKey === 'in-game' ? 'in-game' : (statusKey === 'online' ? 'online' : ''); const itemWrap = el('div', { className: 'sf-fp-item-wrap' }); const item = el('a', { className: 'sf-fp-item', href: f.profileurl, target: '_blank', rel: 'noopener' }); item.dataset.steamId = steamId; item.innerHTML = ` ${buildAvatarHtml(f.avatar, '', statusClass)}
${esc(f.personaname)}
${statusKey === 'in-game' ? `🎮 ${esc(f.gameextrainfo || '游戏中')}` : ''} ${f.countryCode ? `${flagSvg(f.countryCode)}` : ''} ${buildDaysTagHtml(formatDaysSince(f.friend_since))}
`; itemWrap.appendChild(item); const actBtn = el('button', { className: 'sf-fp-action-btn', title: '查看动态' }); actBtn.innerHTML = ``; actBtn.dataset.steamId = steamId; actBtn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); if (steamId) openFriendActivity(steamId, f.personaname, f.avatar); }); itemWrap.appendChild(actBtn); listWrap.appendChild(itemWrap); }); list.appendChild(listWrap); } async function enhanceApiFriendsList(friends) { const steamIds = friends.map(f => f.steamid).filter(Boolean); if (!steamIds.length) return; const apiKey = getApiKey(); if (apiKey) { const bans = await fetchPlayerBansApi(steamIds); if (bans) { Object.keys(bans).forEach(sid => { const ban = bans[sid]; if (ban && (ban.vac || ban.gameBan || ban.communityBanned)) { const host = document.querySelector(`.sf-fp-shield-host[data-sfid="${sid}"]`); if (host) host.outerHTML = buildShieldHtml(ban); } }); } } await runWithConcurrency(steamIds, 6, async (sid) => { const info = await fetchMiniprofile(sid); if (!info) return; const old = state.friendExtras.get(sid) || {}; state.friendExtras.set(sid, { ...old, ...info }); // 更新等级 if (info.level != null) { updateFriendLevelDisplay(sid, info.level); } // 更新游戏数 if (info.gameCount != null) { const host = document.querySelector(`.sf-fp-games-host[data-sfid="${sid}"]`); if (host) { host.style.display = ''; host.textContent = `🎮 ${info.gameCount}`; } } }); // API Key 可用时,补充获取 miniprofile 未拿到等级的好友 if (getApiKey()) { const noLevelIds = steamIds.filter(sid => { const info = state.friendExtras.get(sid); return !info || info.level == null; }); if (noLevelIds.length) { await runWithConcurrency(noLevelIds, 10, async (sid) => { const level = await fetchSteamLevelApi(sid); if (level != null) { const old = state.friendExtras.get(sid) || {}; state.friendExtras.set(sid, { ...old, level }); updateFriendLevelDisplay(sid, level); } }); } } updateFriendStatsPill(steamIds); } function renderFriendsFromApi(list, data) { list.innerHTML = ''; const { own, friends } = data; if (own && own.steamid) state.mySteamId = own.steamid; if (own && own.steamid) renderOwnCardApi(list, own); // 填充个人卡片中的好友数 setTimeout(() => { const friendsCountEl = document.getElementById('sf-fp-own-friends'); if (friendsCountEl && friends) friendsCountEl.textContent = String(friends.length); }, 0); if (friends && friends.length) { const stats = el('div', { className: 'sf-fp-stats' }); stats.innerHTML = `
${friends.length} 位好友
`; list.appendChild(stats); const ingame = friends.filter(f => f.gameextrainfo); const online = friends.filter(f => !f.gameextrainfo && f.personastate > 0); const offline = friends.filter(f => !f.gameextrainfo && f.personastate === 0); if (ingame.length) renderApiFriendSection(list, 'cat-ingame', '🎮', '游戏中的好友', ingame, 'in-game'); if (online.length) renderApiFriendSection(list, 'cat-online', '🟢', '在线好友', online, 'online'); if (offline.length) renderApiFriendSection(list, 'cat-offline', '⚪', '离线好友', offline, 'offline'); enhanceApiFriendsList(friends); } else if (!list.children.length) { list.innerHTML = `
暂无好友数据
`; } } // ==================== 好友浮窗核心(v4.0 重构)==================== function createFriendPanel() { if ($('#sf-friend-btn')) return; // 顶部按钮 const topBtn = el('button', { id: 'sf-top-btn' }); topBtn.innerHTML = `${ICONS.top}回到顶部`; topBtn.title = '回到顶部'; document.body.appendChild(topBtn); const onScroll = () => topBtn.classList.toggle('sf-show', window.scrollY > 400); window.addEventListener('scroll', onScroll, { passive: true }); registerCleanup(() => window.removeEventListener('scroll', onScroll)); topBtn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' })); // 设置入口已移至好友面板头部与游戏库引导(v4.5 精简浮动按钮,移除独立设置 FAB) // 好友按钮 const btn = el('button', { id: 'sf-friend-btn' }); btn.innerHTML = `${ICONS.friends}好友列表`; btn.title = '好友列表'; document.body.appendChild(btn); // 好友面板 const panel = el('div', { id: 'sf-friend-panel' }); panel.innerHTML = `
好友列表
`; document.body.appendChild(panel); btn.addEventListener('click', e => { e.stopPropagation(); if (panel.classList.toggle('open') && !panel.dataset.loaded) { loadFriends().catch(err => console.warn('[SF] loadFriends error:', err)); panel.dataset.loaded = '1'; } }); panel.querySelector('.sf-fp-close').addEventListener('click', e => { e.stopPropagation(); panel.classList.remove('open'); }); // 同步等级与游戏数按钮 panel.querySelector('#sf-fp-sync-stats-btn').addEventListener('click', e => { e.stopPropagation(); syncFriendStats(); }); // 设置按钮(独立设置窗口) panel.querySelector('#sf-fp-settings-btn').addEventListener('click', e => { e.stopPropagation(); openSettingsPanel(); }); // API Key 入口 panel.querySelector('#sf-fp-key-tip').addEventListener('click', e => { e.stopPropagation(); openSettingsPanel(); }); // 外部点击关闭(排除其他浮窗:好友动态、设置、游戏库) const onDocClick = e => { if (panel.contains(e.target) || btn.contains(e.target)) return; const clickedOtherPanel = e.target.closest('#sf-fa-panel, #sf-settings-panel, #sf-lib-panel, .sf-settings-overlay, .sf-userinfo-popup'); if (clickedOtherPanel) return; panel.classList.remove('open'); }; document.addEventListener('click', onDocClick); registerCleanup(() => document.removeEventListener('click', onDocClick)); } // ==================== 好友浮窗渲染 ==================== async function loadFriends() { const list = $('#sf-friend-list'); if (!list) return; // 加载中状态 list.innerHTML = `
正在加载好友数据...
`; try { // ========== 优先 API(参考 friend-manager 1.0.16)========== const steamId = getMySteamId(); const apiKey = getApiKey(); if (apiKey && steamId) { const data = await fetchFriendListApi(steamId); if (data && data.friends) { renderFriendsFromApi(list, data); return; } } // ========== DOM fallback:等待右侧好友栏 ========== const rc = await waitForElementPromise('#friendactivity_right_column', 8000); if (!rc) throw new Error('right_column_not_found'); list.innerHTML = ''; // ========== 1. 个人卡片 ========== const oa = rc.querySelector('.webchat_own_avatar'); const nl = rc.querySelector('.friendslist_entry_content .linkFriend'); if (oa && nl) { const ai = oa.querySelector('img[data-miniprofile],img[srcset]'); const aiSrc = ai?.srcset?.split(',')[0]?.split(' ')[0] || ai?.src || ''; const fi = oa.querySelector('.profile_avatar_frame img')?.src || ''; const ownName = esc(nl.textContent.trim()); const ownHref = nl.href || ''; const lvl = oa.querySelector('.friendPlayerLevelNum, .friend_player_level_wrap .friend_small_level_num, .friend_player_level_num')?.textContent?.trim() || ''; const gameEl = rc.querySelector('.webchat_own_avatar .in-game-name, .webchat_own_avatar .currentGame'); const gameName = gameEl?.textContent?.trim() || ''; // 提取自己的 steamid const ownIdMatch = ownHref.match(/profiles\/(\d{17})/); const ownSteamId = ownIdMatch ? ownIdMatch[1] : ''; if (ownSteamId) state.mySteamId = ownSteamId; const ownCard = el('div', { className: 'sf-fp-own' }); ownCard.innerHTML = `
${ownName} ${fi ? `
` : ''}
${ownName} ${lvl ? `${lvl}` : ``}
- - - -
🆔 ${esc(ownSteamId || '-')} ${gameName ? `🎮 ${esc(gameName)}` : ''}
`; ownCard.querySelectorAll('.sf-fp-own-act').forEach(btn => { btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); const act = btn.dataset.act; if (act === 'chat') { if (typeof LaunchWebChat === 'function') LaunchWebChat(); else showFriendToast('聊天功能未就绪'); } else if (act === 'library') { openPersonalLibrary(ownSteamId); } }); }); list.appendChild(ownCard); // 异步填充个人 hero 统计数据(游戏数/游玩时长/好友数/地区/注册时间/最近游戏) if (ownSteamId) { // 好友数:从页面文本提取 const fsText = rc.querySelector('.profile_link_block_text_friends')?.textContent?.trim() || ''; const friendsMatch = fsText.match(/(\d+)/); const friendsEl = ownCard.querySelector('#sf-fp-own-friends'); if (friendsEl && friendsMatch) friendsEl.textContent = friendsMatch[1]; // 游戏数 + 游玩时长 + 地区 + 注册时间 + 最近游戏:异步获取 (async () => { try { const apiKey = getApiKey(); const [miniInfo, ownedData, summaries] = await Promise.all([ fetchMiniprofile(ownSteamId).catch(() => null), fetchOwnedGamesApi(ownSteamId).catch(() => null), apiKey ? fetchPlayerSummariesApi([ownSteamId]).catch(() => null) : Promise.resolve(null), ]); // 等级:miniprofile 未拿到时用 API 补充 let ownLevel = miniInfo?.level ?? null; if (ownLevel == null) { ownLevel = await fetchLevel(ownSteamId).catch(() => null); } if (ownLevel != null) { const levelEl = ownCard.querySelector('#sf-fp-own-level'); const levelNumEl = ownCard.querySelector('#sf-fp-own-level-num'); if (levelEl) levelEl.style.display = ''; if (levelNumEl) levelNumEl.textContent = String(ownLevel); } const summary = summaries?.[ownSteamId] || {}; if (ownedData && ownedData.games) { const gamesEl = ownCard.querySelector('#sf-fp-own-games'); if (gamesEl) gamesEl.textContent = String(ownedData.total || ownedData.games.length); const totalMin = ownedData.games.reduce((s, g) => s + (g.playtime_forever || 0), 0); const ptEl = ownCard.querySelector('#sf-fp-own-playtime'); if (ptEl) ptEl.textContent = totalMin > 0 ? formatPlaytimeShort(totalMin) : '0h'; // 最近游戏 const recentSorted = [...ownedData.games].filter(g => g.rtime_last_played).sort((a, b) => b.rtime_last_played - a.rtime_last_played); const recentGame = recentSorted[0]; if (recentGame) { const meta = ownCard.querySelector('#sf-fp-own-meta'); if (meta && !meta.querySelector('.sf-fp-recent-game')) { const recentTag = el('span', { className: 'sf-fp-tag sf-fp-recent-game', title: '最近游玩' }); recentTag.innerHTML = `🕹 ${esc(recentGame.name || `App ${recentGame.appid}`)} · ${formatPlaytimeShort(recentGame.playtime_forever)}`; meta.appendChild(recentTag); loadGameZhName(recentTag.querySelector('[data-sf-appid]'), recentGame.appid, recentGame.name || `App ${recentGame.appid}`); } } } if (miniInfo) { if (miniInfo.gameCount != null) { const gamesEl = ownCard.querySelector('#sf-fp-own-games'); if (gamesEl && gamesEl.textContent === '-') gamesEl.textContent = String(miniInfo.gameCount); } if (miniInfo.countryCode) { const countryEl = ownCard.querySelector('#sf-fp-own-country'); if (countryEl) countryEl.innerHTML = flagSvg(miniInfo.countryCode); } if (miniInfo.since) { const sinceEl = ownCard.querySelector('#sf-fp-own-since'); if (sinceEl) { sinceEl.textContent = `🤝 ${esc(miniInfo.since)}`; sinceEl.style.display = ''; } } } if (summary.timeCreated) { const createdEl = ownCard.querySelector('#sf-fp-own-created'); if (createdEl) { const createdDate = new Date(summary.timeCreated * 1000); createdEl.textContent = `📅 注册于 ${createdDate.getFullYear()}-${String(createdDate.getMonth() + 1).padStart(2, '0')}-${String(createdDate.getDate()).padStart(2, '0')}`; createdEl.style.display = ''; } } } catch (e) { console.warn('[SF] own hero stats error:', e); } })(); } } // ========== 2. 统计 pill(游戏数/封禁等)========== const fs = rc.querySelector('.profile_link_block_text_friends'); if (fs && fs.textContent.trim()) { const stats = el('div', { className: 'sf-fp-stats' }); stats.innerHTML = `
${esc(fs.textContent.trim())}
`; list.appendChild(stats); } // ========== 3. 收集所有好友(含 in-game / online / offline)========== const allFriends = Array.from(rc.querySelectorAll('.friend_block_v2, .friendBlock, .friend_block')); const ingame = allFriends.filter(f => f.classList.contains('in-game') || f.querySelector('.in-game, .friend_status_in-game')); const online = allFriends.filter(f => !f.classList.contains('in-game') && (f.classList.contains('online') || f.querySelector('.online, .friend_status_online'))); const offline = allFriends.filter(f => !ingame.includes(f) && !online.includes(f)); // ========== 5. 游戏中好友 ========== if (ingame.length) renderFriendSection(list, 'cat-ingame', '🎮', '游戏中的好友', ingame, 'in-game'); // ========== 6. 在线好友 ========== if (online.length) renderFriendSection(list, 'cat-online', '🟢', '在线好友', online, 'online'); // ========== 7. 离线好友 ========== if (offline.length) renderFriendSection(list, 'cat-offline', '⚪', '离线好友', offline, 'offline'); // ========== 7. 空数据兜底 ========== if (!list.children.length) { list.innerHTML = `
暂无好友数据
`; } else { // 异步加载游戏数 + 封禁 enhanceFriendsList(ingame.concat(online, offline)); } } catch (err) { console.warn('[SF] 好友列表加载失败:', err); let msg = '加载失败,请稍后重试'; let tip = ''; const errMsg = err?.message || String(err); if (errMsg.includes('right_column_not_found')) { msg = '未找到好友列表'; tip = '建议配置 Steam Web API Key,或在好友动态页面使用'; } else if (errMsg.includes('Is your profile set to public') || errMsg.includes('public')) { msg = '好友资料未公开'; tip = '请确保 Steam 个人资料的好友列表为公开状态'; } else if (errMsg.includes('API') || errMsg.includes('key')) { msg = 'API Key 无效或缺失'; tip = '请点击面板右上角设置按钮配置 API Key'; } list.innerHTML = `
${msg}
${tip ? `
${tip}
` : ''}
`; } } // ==================== 单个分类渲染 ==================== function renderFriendSection(list, cls, icon, title, friends, statusKey) { const sec = el('div', { className: `sf-fp-section-title ${cls}` }); sec.innerHTML = `${icon} ${title}${friends.length}`; list.appendChild(sec); const listWrap = el('div', { className: 'sf-fp-list' }); friends.forEach(f => { const av = f.querySelector('.playerAvatar img, .player_avatar img'); const lk = f.querySelector('.friendBlockLinkOverlay, a.friend_block_v2_link'); const lvlEl = f.querySelector('.friend_player_level .friend_player_level_num, .friend_small_level_num, .friendPlayerLevelNum'); const lvl = lvlEl?.textContent?.trim() || ''; // 国家:从好友内容里找 flag const flag = f.querySelector('img[src*="flags/"]'); let countryCode = ''; if (flag) { const m = (flag.getAttribute('src') || '').match(/flags\/([a-z]{2,3})/i); if (m) countryCode = m[1].toUpperCase().slice(0, 2); } // 好友时长 const since = f.querySelector('.friendPlayerSince, .friendSinceDate, .friend_player_since, .friend_since')?.textContent?.trim() || ''; // 当前游戏(游戏中好友) const gm = f.querySelector('.linkFriend_in-game, .friend_status_in-game, .friend_in-game'); const gameName = gm?.textContent?.replace(/游戏中\n|非 Steam 游戏中\n|In-Game\n/g, '').trim() || ''; // 名称 const n = f.querySelector('.friend_block_content .friend_name, .friendBlockContent')?.firstChild?.textContent?.trim() || f.querySelector('.friend_block_content .friend_name')?.textContent?.trim() || f.querySelector('.friendBlockContent a')?.textContent?.trim() || f.querySelector('.friendBlockContent')?.textContent?.split('\n')[0]?.trim() || f.dataset?.steamid || lk?.getAttribute('href')?.split('/').pop()?.replace(/id\/|profiles\//, '') || '未知'; // steamid let steamId = f.dataset?.steamid || ''; if (!steamId) { const href = lk?.getAttribute('href') || ''; const idMatch = href.match(/profiles\/(\d{17})/) || href.match(/id\/([^/]+)/); steamId = idMatch ? (idMatch[1] || '') : ''; } if (steamId && /^\d{17}$/.test(steamId)) { // 缓存到 friendExtras 待异步填充 if (!state.friendExtras.has(steamId)) { state.friendExtras.set(steamId, { loading: true }); } } const statusClass = statusKey === 'in-game' ? 'in-game' : (statusKey === 'online' ? 'online' : ''); // 容器:div 包 a + 按钮(按钮不嵌在 a 里,避免非法 HTML) const itemWrap = el('div', { className: 'sf-fp-item-wrap' }); const item = el('a', { className: 'sf-fp-item', href: href || '#', target: '_blank', rel: 'noopener' }); item.dataset.steamId = steamId; item.innerHTML = ` ${buildAvatarHtml(av?.src, '', statusClass)}
${esc(n)} ${lvl > 1 ? `★ ${lvl}` : '★ -'}
${statusKey === 'in-game' ? `🎮 ${esc(gameName || '游戏中')}` : ''} ${countryCode ? `${flagSvg(countryCode)}` : ''} ${buildDaysTagHtml(since)}
`; itemWrap.appendChild(item); // 右侧"动态"按钮(与 a 同级,独立元素) const actBtn = el('button', { className: 'sf-fp-action-btn', title: '查看动态' }); actBtn.innerHTML = ``; actBtn.dataset.steamId = steamId; actBtn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); if (steamId) openFriendActivity(steamId, n, av?.src); }); itemWrap.appendChild(actBtn); listWrap.appendChild(itemWrap); }); list.appendChild(listWrap); } // ==================== 异步增强好友列表(游戏数 + 封禁)==================== async function enhanceFriendsList(friends) { // 收集 steamid const steamIds = []; friends.forEach(f => { const lk = f.querySelector('.friendBlockLinkOverlay'); const href = lk?.getAttribute('href') || ''; const m = href.match(/profiles\/(\d{17})/); if (m) steamIds.push(m[1]); }); if (!steamIds.length) return; const apiKey = getApiKey(); // 走 API Key 路径:批量拉 bans + summaries if (apiKey) { // bans const bans = await fetchPlayerBansApi(steamIds); if (bans) { Object.keys(bans).forEach(sid => { const ban = bans[sid]; if (ban && (ban.vac || ban.gameBan || ban.communityBanned)) { const host = document.querySelector(`.sf-fp-shield-host[data-sfid="${sid}"]`); if (host) host.outerHTML = buildShieldHtml(ban); } }); } // summaries(含游戏名、状态、国家) const summaries = await fetchPlayerSummariesApi(steamIds); if (summaries) { Object.keys(summaries).forEach(sid => { const sum = summaries[sid]; if (sum && sum.countryCode) { // 已经通过 flag 拿到了,不再二次插入 } }); } } // 每个好友单独拉 miniprofile 拿游戏数(无 key 也可用) await runWithConcurrency(steamIds, 6, async (sid) => { const info = await fetchMiniprofile(sid); if (!info) return; // 写入缓存 const old = state.friendExtras.get(sid) || {}; state.friendExtras.set(sid, { ...old, ...info }); // 更新等级 if (info.level != null) { updateFriendLevelDisplay(sid, info.level); } // 更新游戏数 if (info.gameCount != null) { const host = document.querySelector(`.sf-fp-games-host[data-sfid="${sid}"]`); if (host) { host.style.display = ''; host.textContent = `🎮 ${info.gameCount}`; } } // 更新 shield(如果 API 没拿到) if (!apiKey) { // miniprofile 里通常没封禁信息,留空 } }); // API Key 可用时,补充获取 miniprofile 未拿到等级的好友 if (apiKey) { const noLevelIds = steamIds.filter(sid => { const info = state.friendExtras.get(sid); return !info || info.level == null; }); if (noLevelIds.length) { await runWithConcurrency(noLevelIds, 10, async (sid) => { const level = await fetchSteamLevelApi(sid); if (level != null) { const old2 = state.friendExtras.get(sid) || {}; state.friendExtras.set(sid, { ...old2, level }); updateFriendLevelDisplay(sid, level); } }); } } // 统计 pill:游戏数 updateFriendStatsPill(steamIds); } function updateFriendStatsPill(steamIds) { const host = $('#sf-fp-stats-pills'); if (!host) return; let totalGames = 0, withCount = 0; steamIds.forEach(sid => { const info = state.friendExtras.get(sid); if (info && info.gameCount != null) { totalGames += info.gameCount; withCount++; } }); let html = `🎮 ${totalGames}`; // 封禁统计 const apiKey = getApiKey(); if (apiKey) { const banned = document.querySelectorAll('.sf-fp-shield.banned, .sf-fp-shield.banned-both').length; if (banned) html += `⛔ ${banned}`; } host.innerHTML = html; } // ==================== 同步好友等级与游戏数 ==================== async function syncFriendStats() { const syncBtn = $('#sf-fp-sync-stats-btn'); const items = $$('.sf-fp-item[data-steamid]'); if (!items.length) { showFriendToast('暂无好友数据可同步'); return; } const steamIds = [...new Set(Array.from(items).map(el => el.dataset.steamId).filter(Boolean))]; if (syncBtn) { syncBtn.classList.add('sf-fp-syncing'); syncBtn.disabled = true; } showFriendToast(`开始同步 ${steamIds.length} 位好友的等级与游戏数...`); try { await runWithConcurrency(steamIds, 6, async (sid) => { const info = await fetchMiniprofile(sid); const old = state.friendExtras.get(sid) || {}; if (info) state.friendExtras.set(sid, { ...old, ...info }); // 更新等级:miniprofile 易出现 0/1 异常,优先 API 校验 let level = info?.level ?? null; if (getApiKey() && (level == null || level <= 1)) { const apiLevel = await fetchSteamLevelApi(sid); if (apiLevel != null && apiLevel > 0) level = apiLevel; } if (level != null && level > 0) { const old2 = state.friendExtras.get(sid) || {}; state.friendExtras.set(sid, { ...old2, level }); updateFriendLevelDisplay(sid, level); } // 更新游戏数 if (info?.gameCount != null) { const gamesHost = document.querySelector(`.sf-fp-games-host[data-sfid="${sid}"]`); if (gamesHost) { gamesHost.style.display = ''; gamesHost.textContent = `🎮 ${info.gameCount}`; } } }); showFriendToast('同步完成'); } catch (e) { console.warn('[SF] syncFriendStats error:', e); showFriendToast('同步失败,请稍后重试'); } finally { if (syncBtn) { syncBtn.classList.remove('sf-fp-syncing'); syncBtn.disabled = false; } updateFriendStatsPill(steamIds); } } // ==================== 好友动态浮窗 ==================== function ensureFaPanel() { let panel = $('#sf-fa-panel'); if (panel) return panel; panel = el('div', { id: 'sf-fa-panel' }); panel.innerHTML = `
动态
`; document.body.appendChild(panel); panel.querySelector('#sf-fa-close').addEventListener('click', e => { e.stopPropagation(); closeFaPanel(); }); panel.querySelectorAll('.sf-fa-tab').forEach(btn => { btn.addEventListener('click', () => { panel.querySelectorAll('.sf-fa-tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.faPanel.tab = btn.dataset.tab; renderFaTab(); }); }); // ESC 关闭 const onEsc = e => { if (e.key === 'Escape' && state.faPanel.open) closeFaPanel(); }; document.addEventListener('keydown', onEsc); registerCleanup(() => document.removeEventListener('keydown', onEsc)); return panel; } async function openFriendActivity(steamId, name, avatar) { if (!steamId || !/^\d{17}$/.test(steamId)) { showFriendToast('无效的好友 ID'); return; } // v4.5:未设置 Steam Web API Key 时,先弹设置浮窗,保存后自动打开好友动态 if (!getApiKey()) { showFriendToast('请先设置 Steam Web API Key'); openSettingsPanel(() => openFriendActivity(steamId, name, avatar)); return; } state.faPanel.open = true; state.faPanel.steamId = steamId; state.faPanel.tab = 'overview'; state.faPanel.subcat = 'all'; state.faPanel.data = null; state.faPanel.delisted = null; const panel = ensureFaPanel(); // 标题 const titleEl = panel.querySelector('#sf-fa-title'); titleEl.innerHTML = ` ${esc(name || '动态')} `; // 复位 tab panel.querySelectorAll('.sf-fa-tab').forEach(b => b.classList.remove('active')); panel.querySelector('.sf-fa-tab[data-tab="overview"]').classList.add('active'); // 显示 panel.classList.add('sfp-open'); await renderFaTab(); } function closeFaPanel() { state.faPanel.open = false; const panel = $('#sf-fa-panel'); if (panel) panel.classList.remove('sfp-open'); } // ==================== 好友动态 tab 渲染 ==================== async function renderFaTab() { const body = $('#sf-fa-body'); if (!body) return; const tab = state.faPanel.tab; const steamId = state.faPanel.steamId; body.innerHTML = `
正在加载…
`; if (tab === 'overview') return renderFaOverview(body, steamId); if (tab === 'library') return renderFaLibrary(body, steamId); if (tab === 'playtime') return renderFaPlaytime(body, steamId); if (tab === 'recent') return renderFaRecent(body, steamId); } async function renderFaOverview(body, steamId) { try { // 优先用 miniprofile(快) const info = await fetchMiniprofile(steamId); // 摘要 const sumReq = getApiKey() ? fetchPlayerSummariesApi([steamId]) : Promise.resolve(null); const banReq = getApiKey() ? fetchPlayerBansApi([steamId]) : Promise.resolve(null); const [sum, ban] = await Promise.all([sumReq, banReq]); const s = sum?.[steamId] || {}; const b = ban?.[steamId] || null; // 等级 / 国家 / 状态 const level = info?.level ?? null; const gameCount = info?.gameCount ?? null; const country = info?.countryCode || s.countryCode || ''; const countryName = info?.countryName || country; const status = info?.status || (s.status === 1 ? 'online' : s.status === 2 ? 'busy' : s.status === 3 ? 'away' : 'offline'); const gameName = info?.gameName || s.gameName || ''; const since = info?.since || ''; const frame = info?.frame || ''; const avatarUrl = s.avatar || ''; const name = s.name || $('#sf-fa-title .sf-fa-name')?.textContent || ''; const profileUrl = s.profileUrl || `https://steamcommunity.com/profiles/${steamId}`; // 总游玩时长 + 游戏数(优先 API,无 Key 则 scraping) let totalMinutes = 0; let ownedGameCount = gameCount; const owned = await fetchOwnedGamesApi(steamId).catch(() => null); if (owned?.games) { totalMinutes = owned.games.reduce((s, g) => s + (g.playtime_forever || 0), 0); if (ownedGameCount == null) ownedGameCount = owned.total || owned.games.length; } body.innerHTML = ''; // Hero 卡片 const hero = el('div', { className: 'sf-fa-hero' }); hero.innerHTML = `
${esc(name)}
${esc(name)}
${statusLabel(status)} ${gameName ? `🎮 ${esc(gameName)}` : ''} ${country ? buildCountryFlagHtml(country, countryName) : ''} ${b && (b.vac || b.gameBan || b.communityBanned) ? buildShieldHtml(b) : ''}
`; body.appendChild(hero); // KPI 网格 const grid = el('div', { className: 'sf-fa-stats-grid' }); grid.innerHTML = `
${ownedGameCount != null ? ownedGameCount : '-'}
游戏数
${level != null ? level : '-'}
等级
${totalMinutes ? formatPlaytimeShort(totalMinutes) : '-'}
总时长
${b ? (b.vacCount + b.gameBanCount) : (b ? '有' : '无')}
封禁
`; body.appendChild(grid); // 详细 section const sec = el('div', { className: 'sf-fa-section' }); sec.innerHTML = `
${ICONS.shield}详细信息
`; const rows = []; if (s.lastLogoff) { const last = new Date(s.lastLogoff * 1000); rows.push(`
最后离线${last.toLocaleString()}
`); } if (s.timeCreated) { const tc = new Date(s.timeCreated * 1000); rows.push(`
注册时间${tc.toLocaleDateString()}
`); } if (since) rows.push(`
好友时长${esc(since)}
`); if (b) { if (b.vacCount) rows.push(`
VAC 封禁${b.vacCount} 次
`); if (b.gameBanCount) rows.push(`
游戏封禁${b.gameBanCount} 次
`); if (b.communityBanned) rows.push(`
社区封禁
`); if (b.daysSinceLastBan) rows.push(`
距上次封禁${b.daysSinceLastBan} 天
`); } if (!getApiKey() && !b) { rows.push(`
封禁记录填 API Key 解锁
`); } sec.insertAdjacentHTML('beforeend', rows.join('') || '
暂无更多信息
'); // 跳转链接 sec.insertAdjacentHTML('beforeend', ` `); body.appendChild(sec); } catch (err) { console.error('[SF] 好友概览加载失败:', err); body.innerHTML = `
加载失败,请稍后重试
`; } } async function renderFaPlaytime(body, steamId) { try { let data = null; if (getApiKey()) { data = await fetchOwnedGamesApi(steamId); } else { // fallback scraping const fallback = await fetchGamesPage(steamId, 'all'); data = fallback.total ? fallback : null; } if (!data || !data.games?.length) { body.innerHTML = `
${getApiKey() ? '暂无游戏数据' : '需要 Steam API Key 才能拉取完整时长数据'}
${!getApiKey() ? '
点击底部 "API Key" 申请免费 Key
' : ''}
`; return; } body.innerHTML = ''; // Top 20 const sorted = [...data.games].sort((a, b) => b.playtime_forever - a.playtime_forever); const top = sorted.slice(0, 20); const maxPt = top.length ? top[0].playtime_forever : 1; const sec = el('div', { className: 'sf-fa-section' }); sec.innerHTML = `
${ICONS.trending}Top 20 游玩时长
`; top.forEach((g, i) => { const pct = maxPt > 0 ? (g.playtime_forever / maxPt * 100).toFixed(1) : '0'; const ptH = g.playtime_forever > 0 ? (g.playtime_forever / 60).toFixed(1) + 'h' : '0h'; const rankClass = i === 0 ? 'r1' : i === 1 ? 'r2' : i === 2 ? 'r3' : ''; const row = el('a', { className: 'sf-fa-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); row.innerHTML = `
${i + 1}
${esc(g.name || `App ${g.appid}`)}
⏱ ${ptH} ${g.rtime_last_played ? `${esc(formatLastPlayedShort(g.rtime_last_played))}` : ''}
`; sec.appendChild(row); const _c1 = row.querySelector('.sf-fa-game-icon'); if (_c1) loadCapsuleWithFallback(_c1, g.appid); }); body.appendChild(sec); loadGameZhNamesInContainer(body); } catch (err) { console.error('[SF] 好友时长加载失败:', err); body.innerHTML = `
加载失败
`; } } async function renderFaRecent(body, steamId) { try { let games = null; if (getApiKey()) { games = await fetchRecentlyPlayedApi(steamId); } if (!games || !games.length) { body.innerHTML = `
${getApiKey() ? '该好友最近 2 周未玩游戏' : '需要 Steam API Key 才能拉取最近游玩数据'}
${!getApiKey() ? '
点击底部 "API Key" 申请免费 Key
' : ''}
`; return; } body.innerHTML = ''; const sec = el('div', { className: 'sf-fa-section' }); sec.innerHTML = `
${ICONS.activity}最近 2 周游玩
`; games.forEach(g => { const recent = g.playtime_2weeks > 0 ? formatPlaytimeShort(g.playtime_2weeks) : '-'; const total = g.playtime_forever > 0 ? formatPlaytimeShort(g.playtime_forever) : '-'; const row = el('a', { className: 'sf-fa-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); row.innerHTML = `
${esc(g.name || `App ${g.appid}`)}
📈 2周: ${recent} ⏱ 总: ${total}
`; sec.appendChild(row); const _c2 = row.querySelector('.sf-fa-game-icon'); if (_c2) loadCapsuleWithFallback(_c2, g.appid); }); body.appendChild(sec); loadGameZhNamesInContainer(body); } catch (err) { console.error('[SF] 好友最近加载失败:', err); body.innerHTML = `
加载失败
`; } } async function renderFaLibrary(body, steamId) { try { // 先拉 owned games body.innerHTML = `
正在加载游戏库…
`; let owned = null; if (getApiKey()) owned = await fetchOwnedGamesApi(steamId); if (!owned) owned = await fetchGamesPage(steamId, 'all'); if (!owned || !owned.games?.length) { body.innerHTML = `
${getApiKey() ? '该好友游戏库为空或无权限' : '需要 Steam API Key 才能拉取完整游戏库'}
${!getApiKey() ? '
点击底部 "设置" 申请免费 Key
' : ''}
`; return; } const ownedCount = owned.total || owned.games.length; state.faPanel.data = owned; body.innerHTML = ''; // 绝版数据缓存(懒加载) let delistedData = state.faPanel.delisted; let delistedLoaded = !!delistedData; // 年度大作数据缓存(懒加载) let annualData = state.faPanel.annual; let annualLoaded = !!annualData; // 子分类标签栏 const subcatRow = el('div', { className: 'sf-fa-subcats' }); const allTab = el('button', { className: 'sf-fa-subcat active' }); allTab.innerHTML = `${ICONS.library}全部库存 (${ownedCount})`; allTab.dataset.subcat = 'all'; const delistedTab = el('button', { className: 'sf-fa-subcat' }); delistedTab.innerHTML = `${ICONS.shield}绝版游戏`; delistedTab.dataset.subcat = 'delisted'; const annualTab = el('button', { className: 'sf-fa-subcat' }); annualTab.innerHTML = `${ICONS.award}年度大作`; annualTab.dataset.subcat = 'annual'; subcatRow.appendChild(allTab); subcatRow.appendChild(delistedTab); subcatRow.appendChild(annualTab); body.appendChild(subcatRow); // 内容容器 const contentEl = el('div', { id: 'sf-fa-lib-content' }); body.appendChild(contentEl); // ==================== 全部库存渲染 ==================== const renderAllGames = () => { const total = ownedCount; const totalMin = owned.games.reduce((s, g) => s + (g.playtime_forever || 0), 0); const unplayed = owned.games.filter(g => !g.playtime_forever).length; const ownRate = total ? ((total - unplayed) / total * 100).toFixed(0) : '0'; contentEl.innerHTML = ''; // 摘要 const summary = el('div', { className: 'sf-fa-stats-grid' }); summary.innerHTML = `
${total}
总游戏数
${totalMin ? formatPlaytimeShort(totalMin) : '0h'}
总时长
${unplayed}
未玩数
${ownRate}%
已玩率
`; contentEl.appendChild(summary); // 游戏列表(按游玩时长排序) const allGames = [...owned.games].sort((a, b) => (b.playtime_forever || 0) - (a.playtime_forever || 0)); const PAGE_SIZE = 30; let currentPage = 1; const sec = el('div', { className: 'sf-fa-section' }); sec.innerHTML = `
${ICONS.trending}按时长排序
`; const listWrap = el('div', { id: 'sf-fa-all-list' }); sec.appendChild(listWrap); contentEl.appendChild(sec); const renderPage = () => { const totalPages = Math.max(1, Math.ceil(allGames.length / PAGE_SIZE)); if (currentPage > totalPages) currentPage = totalPages; const start = (currentPage - 1) * PAGE_SIZE; const pageItems = allGames.slice(start, start + PAGE_SIZE); listWrap.innerHTML = ''; if (!pageItems.length) { listWrap.innerHTML = `
暂无游戏
`; return; } pageItems.forEach((g, i) => { const rank = start + i + 1; const pt = g.playtime_forever > 0 ? formatPlaytimeShort(g.playtime_forever) : '0h'; const last = g.rtime_last_played ? formatLastPlayedShort(g.rtime_last_played) : '未玩'; const row = el('a', { className: 'sf-fa-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); row.innerHTML = `
${rank}
${esc(g.name || `App ${g.appid}`)}
⏱ ${pt} ${esc(last)}
`; listWrap.appendChild(row); const _c3 = row.querySelector('.sf-fa-game-icon'); if (_c3) loadCapsuleWithFallback(_c3, g.appid); }); loadGameZhNamesInContainer(listWrap); // 分页 const existingPagi = sec.querySelector('.sf-fa-pagination'); if (existingPagi) existingPagi.remove(); if (totalPages > 1) { const pagi = el('div', { className: 'sf-fa-pagination' }); const prevBtn = el('button', { className: 'sf-fa-page-btn' }, '‹'); prevBtn.disabled = currentPage === 1; prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderPage(); } }); pagi.appendChild(prevBtn); const info = el('span', { className: 'sf-fa-page-info' }, `${currentPage} / ${totalPages}`); pagi.appendChild(info); const nextBtn = el('button', { className: 'sf-fa-page-btn' }, '›'); nextBtn.disabled = currentPage === totalPages; nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; renderPage(); } }); pagi.appendChild(nextBtn); sec.appendChild(pagi); } }; renderPage(); }; // ==================== 绝版游戏渲染 ==================== const renderDelistedGames = async () => { // 懒加载绝版数据 if (!delistedLoaded) { contentEl.innerHTML = `
正在检测绝版收藏…
`; delistedData = await fetchDelistedFor(steamId, owned); state.faPanel.delisted = delistedData; delistedLoaded = true; // 更新标签计数 delistedTab.innerHTML = `${ICONS.shield}绝版游戏 (${delistedData.owned.length})`; } contentEl.innerHTML = ''; // 好友动态绝版游戏轻量展示(简化视图,优化性能) if (!delistedData.owned.length) { contentEl.insertAdjacentHTML('beforeend', `
该好友没有绝版游戏
`); return; } renderDelistedSimple(contentEl, delistedData); }; // ==================== 年度大作渲染 ==================== const renderAnnualGames = async () => { // 懒加载年度大作数据 if (!annualLoaded) { contentEl.innerHTML = `
正在加载年度大作数据…
`; annualData = await fetchAnnualAaaFor(steamId, owned); state.faPanel.annual = annualData; annualLoaded = true; // 更新标签计数 annualTab.innerHTML = `${ICONS.award}年度大作 (${annualData.ownedCount})`; } contentEl.innerHTML = ''; if (!annualData.games.length) { contentEl.insertAdjacentHTML('beforeend', `
年度大作数据加载失败
`); return; } renderAnnualEnhanced(contentEl, annualData, 'sf-fa'); }; // ==================== 子分类切换 ==================== let currentSubcat = state.faPanel.subcat || 'all'; const switchSubcat = async (subcat) => { if (currentSubcat === subcat) return; currentSubcat = subcat; state.faPanel.subcat = subcat; subcatRow.querySelectorAll('.sf-fa-subcat').forEach(b => b.classList.remove('active')); const target = subcat === 'delisted' ? delistedTab : subcat === 'annual' ? annualTab : allTab; target.classList.add('active'); if (subcat === 'all') { renderAllGames(); } else if (subcat === 'delisted') { await renderDelistedGames(); } else if (subcat === 'annual') { await renderAnnualGames(); } }; allTab.addEventListener('click', () => switchSubcat('all')); delistedTab.addEventListener('click', () => switchSubcat('delisted')); annualTab.addEventListener('click', () => switchSubcat('annual')); // 初始渲染 if (currentSubcat === 'delisted') { allTab.classList.remove('active'); delistedTab.classList.add('active'); await renderDelistedGames(); } else if (currentSubcat === 'annual') { allTab.classList.remove('active'); annualTab.classList.add('active'); await renderAnnualGames(); } else { renderAllGames(); } } catch (err) { console.error('[SF] 好友游戏库加载失败:', err); body.innerHTML = `
加载失败
`; } } // ==================== 状态辅助 ==================== function statusLabel(s) { return { online: '🟢 在线', busy: '🔴 忙碌', away: '🌙 离开', snooze: '💤 暂离', 'in-game': '🎮 游戏中', offline: '⚫ 离线' }[s] || '⚫ 离线'; } function statusBg(s) { return { online: 'rgba(16,185,129,0.15)', busy: 'rgba(244,63,94,0.15)', away: 'rgba(245,158,11,0.15)', 'in-game': 'rgba(139,92,246,0.15)' }[s] || 'rgba(255,255,255,0.05)'; } function statusColor(s) { return { online: '#34d399', busy: '#f87171', away: '#fbbf24', 'in-game': '#c084fc' }[s] || '#94a3b8'; } // ==================== 个人游戏库浮窗(中央模态)==================== function ensureLibPanel() { let panel = $('#sf-lib-panel'); if (panel) return panel; panel = el('div', { id: 'sf-lib-panel' }); panel.innerHTML = `
${ICONS.library} 个人游戏库
`; document.body.appendChild(panel); panel.querySelector('#sf-lib-close').addEventListener('click', closeLibPanel); panel.querySelectorAll('.sf-lib-tab').forEach(btn => { btn.addEventListener('click', () => { panel.querySelectorAll('.sf-lib-tab').forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.libPanel.tab = btn.dataset.tab; renderLibTab(); }); }); // ESC const onEsc = e => { if (e.key === 'Escape' && state.libPanel.open) closeLibPanel(); }; document.addEventListener('keydown', onEsc); registerCleanup(() => document.removeEventListener('keydown', onEsc)); return panel; } async function openPersonalLibrary(steamId) { steamId = steamId || state.mySteamId || getMySteamId(); if (!steamId) { showFriendToast('无法识别 SteamID,请在设置中配置'); openSettingsPanel(() => openPersonalLibrary()); return; } if (steamId.length < 17 && !/^\d{17}$/.test(steamId)) { // 自定义 URL,需要从 URL 抓 steamid steamId = getMySteamId() || steamId; } if (!/^\d{17}$/.test(steamId)) { showFriendToast('无效的 SteamID'); openSettingsPanel(() => openPersonalLibrary()); return; } // v4.5:未设置 Steam Web API Key 时,先弹设置浮窗,保存后自动打开游戏库 if (!getApiKey()) { showFriendToast('请先设置 Steam Web API Key'); openSettingsPanel(() => openPersonalLibrary(steamId)); return; } state.mySteamId = steamId; state.libPanel.open = true; state.libPanel.tab = state.libPanel.tab || 'library'; state.libPanel.data = null; const panel = ensureLibPanel(); panel.querySelectorAll('.sf-lib-tab').forEach(b => b.classList.remove('active')); const activeTab = panel.querySelector(`.sf-lib-tab[data-tab="${state.libPanel.tab}"]`) || panel.querySelector('.sf-lib-tab[data-tab="library"]'); if (activeTab) activeTab.classList.add('active'); panel.classList.add('sfl-open'); await renderLibTab(); } function closeLibPanel() { state.libPanel.open = false; const panel = $('#sf-lib-panel'); if (panel) panel.classList.remove('sfl-open'); } // ==================== 库主渲染 ==================== async function renderLibTab() { const body = $('#sf-lib-body'); if (!body) return; const tab = state.libPanel.tab; const steamId = state.mySteamId; body.innerHTML = `
正在加载…
`; const statsEl = $('#sf-lib-stats'); if (statsEl) statsEl.style.display = 'none'; // 没有 API Key 时显示引导 if (!getApiKey()) { body.innerHTML = `
填写 Steam Web API Key 即可解锁个人游戏库、时长统计、绝版收藏检测
申请 API Key
无 Key 时,仍可使用 scraping 模式获取大致数据
`; const input = body.querySelector('#sf-lib-key-input'); const save = body.querySelector('#sf-lib-key-save'); const handleSave = () => { const v = input.value.trim(); if (!v) { showFriendToast('请输入 API Key'); return; } setApiKey(v); showFriendToast('API Key 已保存'); renderLibTab(); }; save.addEventListener('click', handleSave); input.addEventListener('keydown', e => { if (e.key === 'Enter') handleSave(); }); return; } // 加载数据 let owned = state.libPanel.data; if (!owned) { owned = await fetchOwnedGamesApi(steamId); if (!owned) { // fallback scraping owned = await fetchGamesPage(steamId, 'all'); } state.libPanel.data = owned; } if (!owned || !owned.games?.length) { body.innerHTML = `
无法获取游戏库(profile 不公开或 API 失败)
请把 Steam 个人资料设为公开:steamcommunity.com → 编辑个人资料 → 我的隐私设置
`; return; } // 更新 stats if (statsEl) { const total = owned.total || owned.games.length; const totalMin = owned.games.reduce((s, g) => s + (g.playtime_forever || 0), 0); const avg = total ? (totalMin / 60 / total).toFixed(1) : '0'; const totalH = (totalMin / 60).toFixed(1); const unplayed = owned.games.filter(g => g.playtime_forever === 0).length; $('#sf-lib-stat-total').textContent = total; $('#sf-lib-stat-time').textContent = totalH; $('#sf-lib-stat-avg').textContent = avg; $('#sf-lib-stat-unplayed').textContent = unplayed; statsEl.style.display = ''; } if (tab === 'library') return renderLibLibrary(body, owned, steamId); if (tab === 'recent') return renderLibRecent(body, owned, steamId); if (tab === 'playtime') return renderLibPlaytime(body, owned); if (tab === 'activity') return renderLibActivity(body, owned, steamId); if (tab === 'delisted') return renderLibDelisted(body, owned, steamId); if (tab === 'annual') return renderLibAnnual(body, owned, steamId); } // ==================== 时长趋势图 ==================== function getPeriodKey(date, mode) { const y = date.getFullYear(); const m = date.getMonth(); if (mode === 'year') return `${y}`; if (mode === 'quarter') return `${y}-Q${Math.floor(m / 3) + 1}`; return `${y}-${String(m + 1).padStart(2, '0')}`; } function renderPlaytimeTrend(games, mode) { const played = games.filter(g => g.rtime_last_played); if (!played.length) return `
暂无趋势数据
`; const buckets = {}; played.forEach(g => { const d = new Date(g.rtime_last_played * 1000); const key = getPeriodKey(d, mode); buckets[key] = (buckets[key] || 0) + (g.playtime_forever / 60); }); const dates = played.map(g => new Date(g.rtime_last_played * 1000)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); const periods = []; let cursor = new Date(minDate); while (cursor <= maxDate) { periods.push(getPeriodKey(cursor, mode)); if (mode === 'year') cursor.setFullYear(cursor.getFullYear() + 1); else if (mode === 'quarter') cursor.setMonth(cursor.getMonth() + 3); else cursor.setMonth(cursor.getMonth() + 1); } const MAX_POINTS = mode === 'month' ? 12 : mode === 'quarter' ? 12 : 10; const displayPeriods = periods.slice(-MAX_POINTS); const displayValues = displayPeriods.map(p => buckets[p] || 0); if (displayValues.every(v => v === 0)) return `
暂无趋势数据
`; const W = 400, H = 200, PAD = 30; const maxV = Math.max(...displayValues) || 1; const n = displayValues.length; const stepX = n > 1 ? (W - PAD * 2) / (n - 1) : 0; const pts = displayValues.map((v, i) => { const x = n > 1 ? PAD + i * stepX : W / 2; const y = H - PAD - (v / maxV) * (H - PAD * 2); return [x, y]; }); const areaPath = `M ${pts[0][0]} ${H - PAD} ` + pts.map(p => `L ${p[0]} ${p[1]}`).join(' ') + ` L ${pts[pts.length - 1][0]} ${H - PAD} Z`; const linePath = `M ${pts.map(p => `${p[0]} ${p[1]}`).join(' L ')}`; const labelStep = Math.max(1, Math.floor(n / 6)); const labels = displayPeriods.map((p, i) => { if (i % labelStep !== 0 && i !== n - 1) return ''; const short = mode === 'year' ? p : p.replace(/^\d{4}-/, ''); return `${esc(short)}`; }).join(''); const yLabels = [0, 0.25, 0.5, 0.75, 1].map(r => { const v = maxV * r; const y = H - PAD - r * (H - PAD * 2); return `${v.toFixed(0)}h`; }).join(''); return ` ${yLabels} ${labels} ${pts.map((p, i) => `${esc(displayPeriods[i])}: ${displayValues[i].toFixed(1)}h`).join('')} `; } // ==================== 时长分布甜甜圈 ==================== function renderPlaytimeDonut(games) { const ranges = [ { label: '1h 内', min: 0.01, max: 1, color: '#94a3b8' }, { label: '1-10h', min: 1, max: 10, color: '#60a5fa' }, { label: '10-50h', min: 10, max: 50, color: '#34d399' }, { label: '50-100h', min: 50, max: 100, color: '#fbbf24' }, { label: '100-500h', min: 100, max: 500, color: '#f97316' }, { label: '500h+', min: 500, max: Infinity, color: '#ef4444' } ]; const zeroCount = games.filter(g => g.playtime_forever === 0).length; const data = [ { label: '0h(未玩)', count: zeroCount, color: '#64748b' }, ...ranges.map(r => ({ label: r.label, color: r.color, count: games.filter(g => { const h = g.playtime_forever / 60; return h >= r.min && h < r.max; }).length })) ]; const total = games.length; if (total === 0) return `
暂无数据
`; const sz = 140, sw = 28, r = (sz - sw) / 2; const cx = sz / 2, cy = sz / 2; const C = 2 * Math.PI * r; let off = 0, segs = ''; for (const d of data) { if (d.count === 0) continue; const dash = (d.count / total) * C; segs += `${d.label}: ${d.count} (${(d.count / total * 100).toFixed(1)}%)`; off += dash; } const legendHtml = data.filter(d => d.count > 0).map(d => `
${d.label}: ${d.count} (${(d.count / total * 100).toFixed(1)}%)
` ).join(''); return `
${segs}${total}总计
${legendHtml}
`; } async function renderLibLibrary(body, owned, steamId) { // 游戏库:展示全部游戏,按最近游玩排序 body.innerHTML = ''; const total = owned.total || owned.games.length; const PAGE_SIZE = 50; let currentPage = 1; const allGames = [...owned.games].sort((a, b) => (b.rtime_last_played || 0) - (a.rtime_last_played || 0)); const container = el('div', { className: 'sf-lib-section' }); container.innerHTML = `
${ICONS.library}全部游戏(${total} 款)
`; const listWrap = el('div', { id: 'sf-lib-library-list' }); container.appendChild(listWrap); body.appendChild(container); const renderPage = () => { const totalPages = Math.max(1, Math.ceil(allGames.length / PAGE_SIZE)); if (currentPage > totalPages) currentPage = totalPages; const start = (currentPage - 1) * PAGE_SIZE; const pageItems = allGames.slice(start, start + PAGE_SIZE); listWrap.innerHTML = ''; if (!pageItems.length) { listWrap.innerHTML = `
暂无游戏
`; return; } pageItems.forEach(g => { const totalH = g.playtime_forever > 0 ? formatPlaytimeShort(g.playtime_forever) : '0h'; const last = g.rtime_last_played ? formatLastPlayedShort(g.rtime_last_played) : '未玩过'; const row = el('a', { className: 'sf-lib-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); row.innerHTML = `
${esc(g.name || `App ${g.appid}`)}
⏱ ${totalH} · ${esc(last)}
`; listWrap.appendChild(row); const _c4 = row.querySelector('.sf-lib-game-icon'); if (_c4) loadCapsuleWithFallback(_c4, g.appid); }); loadGameZhNamesInContainer(listWrap); // 分页 const existingPagi = container.querySelector('.sf-lib-pagination'); if (existingPagi) existingPagi.remove(); if (totalPages > 1) { const pagi = el('div', { className: 'sf-lib-pagination' }); const prevBtn = el('button', { className: 'sf-lib-page-btn' }, '‹'); prevBtn.disabled = currentPage === 1; prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderPage(); } }); pagi.appendChild(prevBtn); const info = el('span', { className: 'sf-lib-page-info' }, `${currentPage} / ${totalPages}`); pagi.appendChild(info); const nextBtn = el('button', { className: 'sf-lib-page-btn' }, '›'); nextBtn.disabled = currentPage === totalPages; nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; renderPage(); } }); pagi.appendChild(nextBtn); container.appendChild(pagi); } }; renderPage(); } async function renderLibRecent(body, owned, steamId) { body.innerHTML = ''; // 最近 8 款游戏(试取 GetRecentlyPlayedGames) const recentSec = el('div', { className: 'sf-lib-section' }); recentSec.innerHTML = `
${ICONS.activity}最近游玩(2 周)
`; recentSec.insertAdjacentHTML('beforeend', `
`); body.appendChild(recentSec); const recent = await fetchRecentlyPlayedApi(steamId); recentSec.querySelector('.sf-lib-loading')?.remove(); if (recent && recent.length) { recent.slice(0, 8).forEach(g => { const recent2 = g.playtime_2weeks > 0 ? formatPlaytimeShort(g.playtime_2weeks) : '-'; const totalH = g.playtime_forever > 0 ? formatPlaytimeShort(g.playtime_forever) : '-'; const row = el('a', { className: 'sf-lib-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); row.innerHTML = `
${esc(g.name || `App ${g.appid}`)}
📈 2w: ${recent2} ⏱ 总: ${totalH}
`; recentSec.appendChild(row); const _c5 = row.querySelector('.sf-lib-game-icon'); if (_c5) loadCapsuleWithFallback(_c5, g.appid); }); loadGameZhNamesInContainer(recentSec); } else { recentSec.insertAdjacentHTML('beforeend', `
最近 2 周未玩游戏
`); } // 按 rtime_last_played 排序的最近游玩 Top 20(全部游戏库) const sorted = [...owned.games].filter(g => g.rtime_last_played).sort((a, b) => b.rtime_last_played - a.rtime_last_played); const top = sorted.slice(0, 20); const topSec = el('div', { className: 'sf-lib-section' }); topSec.innerHTML = `
${ICONS.trending}按最后启动时间排序 Top 20
`; if (!top.length) { topSec.insertAdjacentHTML('beforeend', `
暂无最近游玩记录
`); } else { top.forEach((g, i) => { const totalH = g.playtime_forever > 0 ? formatPlaytimeShort(g.playtime_forever) : '0h'; const last = formatLastPlayedShort(g.rtime_last_played); const row = el('a', { className: 'sf-lib-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); row.innerHTML = `
${esc(g.name || `App ${g.appid}`)}
⏱ ${totalH} · ${esc(last)}
`; topSec.appendChild(row); const _c6 = row.querySelector('.sf-lib-game-icon'); if (_c6) loadCapsuleWithFallback(_c6, g.appid); }); loadGameZhNamesInContainer(topSec); } body.appendChild(topSec); } async function renderLibPlaytime(body, owned) { body.innerHTML = ''; // 分布 + 趋势 双栏 const chartRow = el('div', { className: 'sf-lib-playtime-row' }); const distCol = el('div', { className: 'sf-lib-playtime-col' }); distCol.innerHTML = `
${ICONS.chartBar}时长分布
`; distCol.insertAdjacentHTML('beforeend', renderPlaytimeDonut(owned.games)); chartRow.appendChild(distCol); const trendCol = el('div', { className: 'sf-lib-playtime-col' }); trendCol.innerHTML = `
${ICONS.trending || ICONS.chartBar}时长趋势
${renderPlaytimeTrend(owned.games, 'month')}
`; chartRow.appendChild(trendCol); body.appendChild(chartRow); // 绑定趋势筛选事件 trendCol.querySelector('.sf-lib-trend-filter').addEventListener('click', e => { const btn = e.target.closest('.sf-lib-trend-btn'); if (!btn) return; trendCol.querySelectorAll('.sf-lib-trend-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const mode = btn.dataset.mode; trendCol.querySelector('#sf-lib-trend-chart').innerHTML = renderPlaytimeTrend(owned.games, mode); }); // Top 20 const sorted = [...owned.games].sort((a, b) => b.playtime_forever - a.playtime_forever); const top = sorted.slice(0, 20); const maxPt = top.length ? top[0].playtime_forever : 1; const topSec = el('div', { className: 'sf-lib-section' }); topSec.innerHTML = `
${ICONS.trending}Top 20 游戏
`; if (!top.length) { topSec.insertAdjacentHTML('beforeend', `
暂无数据
`); } else { top.forEach((g, i) => { const pct = maxPt > 0 ? (g.playtime_forever / maxPt * 100).toFixed(1) : '0'; const ptH = g.playtime_forever > 0 ? (g.playtime_forever / 60).toFixed(1) + 'h' : '0h'; const rankClass = i === 0 ? 'r1' : i === 1 ? 'r2' : i === 2 ? 'r3' : ''; const row = el('a', { className: 'sf-lib-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); row.innerHTML = `
${i + 1}
${esc(g.name || `App ${g.appid}`)}
⏱ ${ptH} ${g.rtime_last_played ? `· ${esc(formatLastPlayedShort(g.rtime_last_played))}` : ''}
`; topSec.appendChild(row); const _c7 = row.querySelector('.sf-lib-game-icon'); if (_c7) loadCapsuleWithFallback(_c7, g.appid); }); loadGameZhNamesInContainer(topSec); } body.appendChild(topSec); } async function renderLibActivity(body, owned, steamId) { // 入库列表:展示用户所有入库游戏,按最近游玩时间排序,未玩过置底 body.innerHTML = `
正在整理入库列表…
`; const total = owned.total || owned.games.length; const PAGE_SIZE = 50; // 优先 rtime_last_played;无时视为 0,按名称排序置底 const allGames = [...owned.games].sort((a, b) => { const ta = a.rtime_last_played || 0; const tb = b.rtime_last_played || 0; if (ta !== tb) return tb - ta; return (a.name || `App ${a.appid}`).localeCompare(b.name || `App ${b.appid}`); }); body.innerHTML = ''; // 概要 const summary = el('div', { className: 'sf-fa-stats-grid' }); summary.innerHTML = `
${total}
入库总数
${owned.games.filter(g => g.playtime_forever === 0).length}
未玩过
`; body.appendChild(summary); const container = el('div', { className: 'sf-lib-section' }); container.innerHTML = `
${ICONS.trending}全部入库游戏(${total} 款)
`; const listWrap = el('div', { id: 'sf-lib-activity-list' }); container.appendChild(listWrap); body.appendChild(container); let currentPage = 1; const renderPage = () => { const totalPages = Math.max(1, Math.ceil(allGames.length / PAGE_SIZE)); if (currentPage > totalPages) currentPage = totalPages; const start = (currentPage - 1) * PAGE_SIZE; const pageItems = allGames.slice(start, start + PAGE_SIZE); listWrap.innerHTML = ''; if (!pageItems.length) { listWrap.innerHTML = `
暂无游戏
`; return; } pageItems.forEach(g => { const totalH = g.playtime_forever > 0 ? formatPlaytimeShort(g.playtime_forever) : '0h'; const last = g.rtime_last_played ? formatLastPlayedShort(g.rtime_last_played) : '未玩过'; const unplayed = g.playtime_forever === 0; const row = el('a', { className: 'sf-lib-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); if (unplayed) row.style.opacity = '0.75'; row.innerHTML = `
${esc(g.name || `App ${g.appid}`)}
⏱ ${totalH} · ${esc(last)} ${unplayed ? '未游玩' : ''}
`; listWrap.appendChild(row); const _c8 = row.querySelector('.sf-lib-game-icon'); if (_c8) loadCapsuleWithFallback(_c8, g.appid); }); loadGameZhNamesInContainer(listWrap); // 分页 const existingPagi = container.querySelector('.sf-lib-pagination'); if (existingPagi) existingPagi.remove(); if (totalPages > 1) { const pagi = el('div', { className: 'sf-lib-pagination' }); const prevBtn = el('button', { className: 'sf-lib-page-btn' }, '‹'); prevBtn.disabled = currentPage === 1; prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderPage(); } }); pagi.appendChild(prevBtn); const info = el('span', { className: 'sf-lib-page-info' }, `${currentPage} / ${totalPages}`); pagi.appendChild(info); const nextBtn = el('button', { className: 'sf-lib-page-btn' }, '›'); nextBtn.disabled = currentPage === totalPages; nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; renderPage(); } }); pagi.appendChild(nextBtn); container.appendChild(pagi); } }; renderPage(); } // ==================== 绝版游戏增强展示(参考 Steam-License-Classifier)==================== function renderDelistedEnhanced(container, data, prefix) { const owned = data.owned || []; const total = data.total || 0; const missingCount = total - owned.length; const ratePct = total > 0 ? Math.round((owned.length / total) * 100) : 0; const rateStr = total > 0 ? ((owned.length / total) * 100).toFixed(1) + '%' : '0%'; const ds = { search: '', typeFilter: 'all', sortBy: 'name', sortDir: 'asc', viewMode: 'card', showCover: false, page: 1, pageSize: 30 }; const typeCounts = {}; owned.forEach(g => { typeCounts[g.delistedType] = (typeCounts[g.delistedType] || 0) + 1; }); container.innerHTML = ''; // 统计卡片 const summary = el('div', { className: 'sf-lib-stats' }); summary.innerHTML = `
${total}
绝版总数
${owned.length}
已拥有
${missingCount}
未拥有
${rateStr}
拥有率
`; container.appendChild(summary); // 进度条 const progress = el('div', { className: 'sf-delisted-progress' }); progress.innerHTML = `
`; container.appendChild(progress); // 类型筛选 const typePills = el('div', { className: 'sf-fa-delisted-type-pills' }); const allPill = el('button', { className: 'sf-fa-delisted-type-pill active' }, `全部 (${owned.length})`); allPill.dataset.type = 'all'; typePills.appendChild(allPill); Object.keys(typeCounts).sort((a, b) => typeCounts[b] - typeCounts[a]).forEach(tk => { const pill = el('button', { className: 'sf-fa-delisted-type-pill' }, `${getDelistedTypeLabel(tk)} (${typeCounts[tk]})`); pill.dataset.type = tk; typePills.appendChild(pill); }); container.appendChild(typePills); // 工具栏 const toolbar = el('div', { className: 'sf-delisted-toolbar' }); toolbar.innerHTML = `
`; container.appendChild(toolbar); // 列表容器 const listSec = el('div', { className: prefix === 'sf-fa' ? 'sf-fa-section' : 'sf-lib-section' }); listSec.id = `${prefix}-delisted-list`; container.appendChild(listSec); const getFiltered = () => { let list = owned; if (ds.typeFilter !== 'all') list = list.filter(g => g.delistedType === ds.typeFilter); if (ds.search) { const q = ds.search.toLowerCase(); list = list.filter(g => (g.name || '').toLowerCase().includes(q) || String(g.appid).includes(q)); } const dir = ds.sortDir === 'asc' ? 1 : -1; list = [...list].sort((a, b) => { if (ds.sortBy === 'name') return dir * (a.name || '').localeCompare(b.name || '', 'zh-CN'); if (ds.sortBy === 'date') return dir * ((a.delistedDate || '').localeCompare(b.delistedDate || '')); if (ds.sortBy === 'rarity') { const pa = parseFloat(a.ownersPct) || 0; const pb = parseFloat(b.ownersPct) || 0; return dir * (pa - pb); } return 0; }); return list; }; const renderList = () => { const list = getFiltered(); const totalPages = Math.max(1, Math.ceil(list.length / ds.pageSize)); if (ds.page > totalPages) ds.page = totalPages; const start = (ds.page - 1) * ds.pageSize; const pageItems = list.slice(start, start + ds.pageSize); listSec.innerHTML = ''; if (!pageItems.length) { const emptyClass = prefix === 'sf-fa' ? 'sf-fa-empty-block' : 'sf-lib-empty-block'; listSec.innerHTML = `
暂无匹配数据
`; return; } if (ds.viewMode === 'card') { const grid = el('div', { className: 'sf-delisted-grid' }); pageItems.forEach(g => { const card = el('div', { className: 'sf-delisted-card' }); card.innerHTML = ` 已拥有
${esc(g.name)}
${getDelistedTypeLabel(g.delistedType)} ${g.ownersPct ? `${esc(g.ownersPct)}` : ''} ${g.delistedDate ? `${esc(g.delistedDate)}` : ''} ${g.achievements > 0 ? `🏆 ${g.achievements}` : ''}
`; // 封面多CDN兜底加载(占位div替换,避免破图错位) const coverImg = card.querySelector('.sf-delisted-cover-img'); if (coverImg) loadCapsuleWithFallback(coverImg, g.appid); card.addEventListener('click', () => window.open(GAME_STORE_URL(g.appid), '_blank')); grid.appendChild(card); }); listSec.appendChild(grid); } else { const listWrap = el('div', { className: 'sf-delisted-list' }); pageItems.forEach(g => { const row = el('a', { className: `sf-delisted-list-item${ds.showCover ? ' has-cover' : ''}`, href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); const metaHtml = `
${getDelistedTypeLabel(g.delistedType)} ${g.ownersPct ? `${esc(g.ownersPct)}` : ''} ${g.achievements > 0 ? `🏆 ${g.achievements}` : ''}
`; if (ds.showCover) { // 列表封面用多CDN兜底,失败替换占位div保持尺寸,不再 display:none 错位 row.innerHTML = `
${esc(g.name)}
${metaHtml} `; const coverImg = row.querySelector('.sf-delisted-cover'); if (coverImg) loadCapsuleWithFallback(coverImg, g.appid); } else { row.innerHTML = `
${esc(g.name)}
${metaHtml} `; } listWrap.appendChild(row); }); listSec.appendChild(listWrap); } if (totalPages > 1) { const pagi = el('div', { className: prefix === 'sf-fa' ? 'sf-fa-pagination' : 'sf-lib-pagination' }); const prevBtn = el('button', { className: prefix === 'sf-fa' ? 'sf-fa-page-btn' : 'sf-lib-page-btn' }, '‹'); prevBtn.disabled = ds.page === 1; prevBtn.addEventListener('click', () => { if (ds.page > 1) { ds.page--; renderList(); } }); pagi.appendChild(prevBtn); const info = el('span', { className: prefix === 'sf-fa' ? 'sf-fa-page-info' : 'sf-lib-page-info' }, `${ds.page} / ${totalPages}`); pagi.appendChild(info); const nextBtn = el('button', { className: prefix === 'sf-fa' ? 'sf-fa-page-btn' : 'sf-lib-page-btn' }, '›'); nextBtn.disabled = ds.page === totalPages; nextBtn.addEventListener('click', () => { if (ds.page < totalPages) { ds.page++; renderList(); } }); pagi.appendChild(nextBtn); listSec.appendChild(pagi); } loadGameZhNamesInContainer(listSec); }; const searchInput = toolbar.querySelector('.sf-delisted-search'); let searchTimer; searchInput.addEventListener('input', e => { ds.search = e.target.value; ds.page = 1; clearTimeout(searchTimer); searchTimer = setTimeout(renderList, 200); }); toolbar.addEventListener('click', e => { const btn = e.target.closest('.sf-delisted-sort-btn, .sf-delisted-view-btn, .sf-delisted-cover-btn'); if (!btn) return; if (btn.classList.contains('sf-delisted-sort-btn')) { const sortBy = btn.dataset.sort; if (ds.sortBy === sortBy) ds.sortDir = ds.sortDir === 'asc' ? 'desc' : 'asc'; else { ds.sortBy = sortBy; ds.sortDir = 'asc'; } ds.page = 1; toolbar.querySelectorAll('.sf-delisted-sort-btn').forEach(b => b.classList.toggle('active', b.dataset.sort === ds.sortBy)); renderList(); } else if (btn.classList.contains('sf-delisted-view-btn')) { ds.viewMode = btn.dataset.view; toolbar.querySelectorAll('.sf-delisted-view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === ds.viewMode)); renderList(); } else if (btn.classList.contains('sf-delisted-cover-btn')) { ds.showCover = !ds.showCover; btn.classList.toggle('active', ds.showCover); renderList(); } }); typePills.addEventListener('click', e => { const pill = e.target.closest('.sf-fa-delisted-type-pill'); if (!pill) return; typePills.querySelectorAll('.sf-fa-delisted-type-pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); ds.typeFilter = pill.dataset.type; ds.page = 1; renderList(); }); renderList(); } // ==================== 好友动态绝版游戏轻量版(简化视图,优化性能)==================== function renderDelistedSimple(container, data) { const owned = data.owned || []; const total = data.total || 0; const missingCount = total - owned.length; const rateStr = total > 0 ? ((owned.length / total) * 100).toFixed(1) + '%' : '0%'; let activeFilter = 'all'; let currentPage = 1; const PAGE_SIZE = 30; const typeCounts = {}; owned.forEach(g => { typeCounts[g.delistedType] = (typeCounts[g.delistedType] || 0) + 1; }); container.innerHTML = ''; // 统计卡片 const summary = el('div', { className: 'sf-lib-stats' }); summary.innerHTML = `
${total}
绝版总数
${owned.length}
已拥有
${missingCount}
未拥有
${rateStr}
拥有率
`; container.appendChild(summary); // 类型筛选 const typePills = el('div', { className: 'sf-fa-delisted-type-pills' }); const allPill = el('button', { className: 'sf-fa-delisted-type-pill active' }, `全部 (${owned.length})`); allPill.dataset.type = 'all'; typePills.appendChild(allPill); Object.keys(typeCounts).sort((a, b) => typeCounts[b] - typeCounts[a]).forEach(tk => { const pill = el('button', { className: 'sf-fa-delisted-type-pill' }, `${getDelistedTypeLabel(tk)} (${typeCounts[tk]})`); pill.dataset.type = tk; typePills.appendChild(pill); }); container.appendChild(typePills); // 列表容器 const listSec = el('div', { className: 'sf-fa-section' }); listSec.id = 'sf-fa-delisted-list'; container.appendChild(listSec); const renderList = () => { const filtered = activeFilter === 'all' ? owned : owned.filter(g => g.delistedType === activeFilter); const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)); if (currentPage > totalPages) currentPage = totalPages; const start = (currentPage - 1) * PAGE_SIZE; const pageItems = filtered.slice(start, start + PAGE_SIZE); listSec.innerHTML = ''; if (!pageItems.length) { listSec.innerHTML = `
该分类暂无数据
`; return; } // 轻量列表:图标 + 名称 + 类型/占比/成就 + 已拥有 pageItems.forEach(g => { const row = el('a', { className: 'sf-fa-game-item', href: GAME_STORE_URL(g.appid), target: '_blank', rel: 'noopener' }); row.innerHTML = `
${esc(g.name)}
${getDelistedTypeLabel(g.delistedType)} ${g.ownersPct ? `${esc(g.ownersPct)}` : ''} ${g.achievements ? `🏆 ${g.achievements}` : ''} ${g.delistedDate ? `${esc(g.delistedDate)}` : ''}
已拥有 `; listSec.appendChild(row); const _c9 = row.querySelector('.sf-fa-game-icon'); if (_c9) loadCapsuleWithFallback(_c9, g.appid); }); // 分页 if (totalPages > 1) { const pagi = el('div', { className: 'sf-fa-pagination' }); const prevBtn = el('button', { className: 'sf-fa-page-btn' }, '‹'); prevBtn.disabled = currentPage === 1; prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; renderList(); } }); pagi.appendChild(prevBtn); const info = el('span', { className: 'sf-fa-page-info' }, `${currentPage} / ${totalPages}`); pagi.appendChild(info); const nextBtn = el('button', { className: 'sf-fa-page-btn' }, '›'); nextBtn.disabled = currentPage === totalPages; nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; renderList(); } }); pagi.appendChild(nextBtn); listSec.appendChild(pagi); } loadGameZhNamesInContainer(listSec); }; renderList(); typePills.querySelectorAll('.sf-fa-delisted-type-pill').forEach(pill => { pill.addEventListener('click', () => { typePills.querySelectorAll('.sf-fa-delisted-type-pill').forEach(p => p.classList.remove('active')); pill.classList.add('active'); activeFilter = pill.dataset.type; currentPage = 1; renderList(); }); }); } async function renderLibDelisted(body, owned, steamId) { body.innerHTML = `
正在加载绝版数据库…
`; const data = state.libPanel.delisted || await fetchDelistedFor(steamId, owned); state.libPanel.delisted = data; body.innerHTML = ''; if (!data.owned.length) { body.innerHTML = `
您的游戏库中暂无绝版游戏
`; return; } renderDelistedEnhanced(body, data, 'sf-lib'); } // ==================== 年度大作渲染(个人游戏库)==================== async function renderLibAnnual(body, owned, steamId) { body.innerHTML = `
正在加载年度大作数据…
`; let data = state.libPanel.annual; if (!data) { data = await fetchAnnualAaaFor(steamId, owned); state.libPanel.annual = data; } renderAnnualEnhanced(body, data, 'sf-lib'); } // ==================== 年度大作增强展示(通用:个人库 + 好友动态)==================== function renderAnnualEnhanced(container, data, prefix) { const allGames = data.games || []; const byYear = data.byYear || {}; const total = data.total || 0; const ownedCount = data.ownedCount || 0; const missingCount = total - ownedCount; // 状态 const ds = { selectedYear: 'all', ownedOnly: false, search: '', page: 1, pageSize: 30, }; container.innerHTML = ''; // 统计卡片 const summary = el('div', { className: 'sf-lib-stats' }); summary.innerHTML = `
${total}
大作总数
${ownedCount}
已拥有
${missingCount}
未拥有
${total > 0 ? ((ownedCount / total) * 100).toFixed(1) : 0}%
拥有率
`; container.appendChild(summary); // 年份选择 + 筛选工具栏 const years = Object.keys(byYear).map(Number).filter(y => !isNaN(y)).sort((a, b) => b - a); const toolbar = el('div', { className: 'sf-fa-subcats', style: 'margin-bottom:10px;flex-wrap:wrap;gap:4px' }); // "全部" 年份按钮 const allYearBtn = el('button', { className: 'sf-fa-subcat active' }); allYearBtn.innerHTML = `全部 (${total})`; allYearBtn.dataset.year = 'all'; toolbar.appendChild(allYearBtn); // 各年份按钮 for (const y of years) { const yearGames = byYear[y] || []; const yearOwned = yearGames.filter(g => g.owned).length; const btn = el('button', { className: 'sf-fa-subcat' }); btn.innerHTML = `${y} (${yearOwned}/${yearGames.length})`; btn.dataset.year = String(y); toolbar.appendChild(btn); } container.appendChild(toolbar); // 已拥有筛选 + 搜索 const filterRow = el('div', { className: 'sf-delisted-toolbar', style: 'margin-bottom:10px' }); filterRow.innerHTML = ` `; container.appendChild(filterRow); // 列表容器 const listSec = el('div', { className: prefix === 'sf-fa' ? 'sf-fa-section' : 'sf-lib-section' }); listSec.id = `${prefix}-annual-list`; container.appendChild(listSec); // 获取过滤后的列表 const getFiltered = () => { let list = allGames; if (ds.selectedYear !== 'all') { list = list.filter(g => String(g.year) === ds.selectedYear); } if (ds.ownedOnly) { list = list.filter(g => g.owned); } if (ds.search) { const q = ds.search.toLowerCase(); list = list.filter(g => (g.name || '').toLowerCase().includes(q) || String(g.appid).includes(q)); } // 按日期排序 list = [...list].sort((a, b) => (a.date || '').localeCompare(b.date || '', 'zh-CN')); return list; }; // 渲染列表 const renderList = () => { const list = getFiltered(); const totalPages = Math.max(1, Math.ceil(list.length / ds.pageSize)); if (ds.page > totalPages) ds.page = totalPages; const start = (ds.page - 1) * ds.pageSize; const pageItems = list.slice(start, start + ds.pageSize); listSec.innerHTML = ''; if (!pageItems.length) { const emptyClass = prefix === 'sf-fa' ? 'sf-fa-empty-block' : 'sf-lib-empty-block'; listSec.innerHTML = `
暂无匹配的大作游戏
`; return; } // 卡片网格视图 const grid = el('div', { className: 'sf-delisted-grid' }); pageItems.forEach(g => { const card = el('div', { className: 'sf-delisted-card' }); if (g.owned) { card.innerHTML = `已拥有`; } else { card.innerHTML = `未拥有`; } card.insertAdjacentHTML('beforeend', `
${esc(g.name)}
${g.date ? `${esc(g.date)}` : ''} ${g.owned && g.playtime_forever > 0 ? `⏱ ${formatPlaytimeShort(g.playtime_forever)}` : ''}
`); // 封面多CDN兜底加载 const coverImg = card.querySelector('.sf-delisted-cover-img'); if (coverImg) loadCapsuleWithFallback(coverImg, g.appid); card.addEventListener('click', () => window.open(GAME_STORE_URL(g.appid), '_blank')); grid.appendChild(card); }); listSec.appendChild(grid); // 分页 if (totalPages > 1) { const pagi = el('div', { className: prefix === 'sf-fa' ? 'sf-fa-pagination' : 'sf-lib-pagination' }); const prevBtn = el('button', { className: prefix === 'sf-fa' ? 'sf-fa-page-btn' : 'sf-lib-page-btn' }, '‹'); prevBtn.disabled = ds.page === 1; prevBtn.addEventListener('click', () => { if (ds.page > 1) { ds.page--; renderList(); } }); pagi.appendChild(prevBtn); const info = el('span', { className: prefix === 'sf-fa' ? 'sf-fa-page-info' : 'sf-lib-page-info' }, `${ds.page} / ${totalPages}`); pagi.appendChild(info); const nextBtn = el('button', { className: prefix === 'sf-fa' ? 'sf-fa-page-btn' : 'sf-lib-page-btn' }, '›'); nextBtn.disabled = ds.page === totalPages; nextBtn.addEventListener('click', () => { if (ds.page < totalPages) { ds.page++; renderList(); } }); pagi.appendChild(nextBtn); listSec.appendChild(pagi); } loadGameZhNamesInContainer(listSec); }; // 年份切换 toolbar.addEventListener('click', e => { const btn = e.target.closest('.sf-fa-subcat'); if (!btn) return; toolbar.querySelectorAll('.sf-fa-subcat').forEach(b => b.classList.remove('active')); btn.classList.add('active'); ds.selectedYear = btn.dataset.year; ds.page = 1; renderList(); }); // 搜索 const searchInput = filterRow.querySelector('.sf-delisted-search'); let searchTimer; searchInput.addEventListener('input', e => { ds.search = e.target.value; ds.page = 1; clearTimeout(searchTimer); searchTimer = setTimeout(renderList, 200); }); // 已拥有筛选 filterRow.addEventListener('click', e => { const btn = e.target.closest('[data-filter="owned"]'); if (!btn) return; ds.ownedOnly = !ds.ownedOnly; btn.classList.toggle('active', ds.ownedOnly); btn.style.cssText = ds.ownedOnly ? 'background:rgba(16,185,129,0.2);border-color:rgba(16,185,129,0.4);color:var(--sf-fp-green-text)' : ''; ds.page = 1; renderList(); }); renderList(); } // ==================== 独立设置窗口 ==================== function ensureSettingsPanel() { let panel = $('#sf-settings-panel'); if (panel) return panel; // 遮罩 const overlay = el('div', { id: 'sf-settings-overlay', className: 'sf-settings-overlay' }); document.body.appendChild(overlay); panel = el('div', { id: 'sf-settings-panel' }); panel.innerHTML = `
${ICONS.settings} 脚本设置
${ICONS.key}Steam Web API Key
填写后可用于获取完整游戏库、游玩时长、封禁状态等更准数据。
${ICONS.users}我的 SteamID
脚本会自动从页面检测 17 位 SteamID,如果检测失败请手动填写(用于个人游戏库、好友统计)。
`; document.body.appendChild(panel); panel.querySelector('#sf-settings-close').addEventListener('click', e => { e.stopPropagation(); closeSettingsPanel(); }); overlay.addEventListener('click', closeSettingsPanel); panel.querySelector('#sf-settings-getkey').addEventListener('click', () => { window.open('https://steamcommunity.com/dev/apikey', '_blank'); }); panel.querySelector('#sf-settings-save').addEventListener('click', async () => { const key = panel.querySelector('#sf-settings-apikey').value.trim(); const status = panel.querySelector('#sf-settings-status'); setApiKey(key); if (key) { // 验证 key 是否有效 try { const resp = await steamApi('/ISteamUser/GetPlayerSummaries/v0002/', { key, steamids: '76561197960434622', format: 'json' }); if (resp?.response?.players?.length >= 0) { status.textContent = 'API Key 已保存,拉取正常。'; status.className = 'sf-settings-status show ok'; } else { throw new Error('invalid response'); } } catch (e) { status.textContent = 'API Key 已保存,但验证失败,可能尚未激活或网络受限。'; status.className = 'sf-settings-status show err'; } } else { status.textContent = '已清空 API Key。'; status.className = 'sf-settings-status show'; } // 保存了有效 Key 且有待执行动作时,关闭设置并自动打开目标浮窗 if (key && state.settingsPanel.pendingAction) { const action = state.settingsPanel.pendingAction; state.settingsPanel.pendingAction = null; closeSettingsPanel(); setTimeout(action, 350); } }); panel.querySelector('#sf-settings-autoid').addEventListener('click', () => { const input = panel.querySelector('#sf-settings-steamid'); const detected = detectSteamIdFromPage(); if (detected) input.value = detected; const status = panel.querySelector('#sf-settings-status'); status.textContent = detected ? '已自动检测到 SteamID。' : '未检测到 SteamID,请手动填写。'; status.className = detected ? 'sf-settings-status show ok' : 'sf-settings-status show err'; }); panel.querySelector('#sf-settings-sid-save').addEventListener('click', () => { const sid = panel.querySelector('#sf-settings-steamid').value.trim(); const status = panel.querySelector('#sf-settings-status'); if (sid && !/^\d{17}$/.test(sid)) { status.textContent = 'SteamID 必须是 17 位数字。'; status.className = 'sf-settings-status show err'; return; } setStoredSteamId(sid); state.mySteamId = sid; status.textContent = sid ? 'SteamID 已保存。' : '已清空手动 SteamID,将使用自动检测。'; status.className = 'sf-settings-status show ok'; }); // ESC 关闭 const onEsc = e => { if (e.key === 'Escape' && state.settingsPanel.open) closeSettingsPanel(); }; document.addEventListener('keydown', onEsc); registerCleanup(() => document.removeEventListener('keydown', onEsc)); return panel; } function detectSteamIdFromPage() { // 优先 g_steamID 全局变量 const gsid = (typeof window.g_steamID !== 'undefined' && window.g_steamID) || (typeof window.g_steamid !== 'undefined' && window.g_steamid) || ''; if (/^\d{17}$/.test(gsid)) return gsid; // URL 路径 const m = (window.location.pathname || '').match(/\/profiles\/(\d{17})/); if (m) return m[1]; // 页面元素 const elLink = document.querySelector('a[href*="/profiles/76561"]'); if (elLink) { const mm = elLink.href.match(/profiles\/(\d{17})/); if (mm) return mm[1]; } return ''; } function openSettingsPanel(pendingAction) { state.settingsPanel.open = true; // 记录待执行动作:保存 API Key 后自动打开目标浮窗(游戏库/好友动态) if (typeof pendingAction === 'function') state.settingsPanel.pendingAction = pendingAction; ensureSettingsPanel(); // 刷新当前值 const panel = $('#sf-settings-panel'); const overlay = $('#sf-settings-overlay'); panel.querySelector('#sf-settings-apikey').value = getApiKey(); panel.querySelector('#sf-settings-steamid').value = getMySteamId(); // 有待执行动作时,提示用户先填写 Key const status = panel.querySelector('#sf-settings-status'); if (state.settingsPanel.pendingAction && status) { status.textContent = '请填写 Steam Web API Key 并保存,保存后将自动打开目标页面。'; status.className = 'sf-settings-status show'; } panel.classList.add('sf-settings-open'); overlay.classList.add('sf-show'); } function closeSettingsPanel() { state.settingsPanel.open = false; const panel = $('#sf-settings-panel'); if (panel) panel.classList.remove('sf-settings-open'); const overlay = $('#sf-settings-overlay'); if (overlay) overlay.classList.remove('sf-show'); } // 兼容旧 API Key 对话框调用 function openApiKeyDialog() { openSettingsPanel(); } // ==================== 库触发按钮 ==================== function createLibraryButton() { if ($('#sf-library-btn')) return; const btn = el('button', { id: 'sf-library-btn' }); btn.innerHTML = `${ICONS.library}我的游戏库`; btn.title = '我的游戏库'; document.body.appendChild(btn); btn.addEventListener('click', () => openPersonalLibrary(state.mySteamId)); } // ==================== 用户信息弹窗(hover 作者名)==================== async function getUserInfo(miniprofile) { if (!miniprofile) return null; try { const cached = cache.get(`user_${miniprofile}`); if (cached) return cached; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT); let response; try { response = await fetch(`https://steamcommunity.com/miniprofile/${miniprofile}`, { signal: controller.signal }); } finally { clearTimeout(timeout); } if (!response.ok) return null; const html = await response.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const levelEl = doc.querySelector('.friend_player_level_wrap .friend_small_level_num'); const level = levelEl ? parseInt(levelEl.textContent, 10) : null; const gameLink = doc.querySelector('a[href*="/games?tab=all"]'); let gameCount = null; if (gameLink) { const match = gameLink.textContent.match(/(\d+)/); gameCount = match ? parseInt(match[1], 10) : null; } const badgesEl = doc.querySelector('a[href*="/badges"]'); let badgeCount = null; if (badgesEl) { const match = badgesEl.textContent.match(/(\d+)/); badgeCount = match ? parseInt(match[1], 10) : null; } const info = { level, gameCount, badgeCount }; cache.set(`user_${miniprofile}`, info); return info; } catch (error) { console.warn('[SF] 获取用户信息失败:', error); return null; } } function createUserInfoPopup(element, miniprofile, userName) { document.querySelector('.sf-userinfo-popup')?.remove(); const popup = el('div', { className: 'sf-userinfo-popup' }, '
加载中...
'); element.style.position = 'relative'; element.appendChild(popup); requestAnimationFrame(() => popup.classList.add('visible')); const renderStat = (value, label) => { if (value == null) return ''; return `
${value}
${label}
`; }; getUserInfo(miniprofile).then(info => { if (!info) { popup.innerHTML = '
加载失败
'; return; } const statsHtml = [ renderStat(info.gameCount, '游戏'), renderStat(info.badgeCount, '徽章'), ].filter(Boolean).join('') || '
-
暂无数据
'; popup.innerHTML = `
${esc(userName)}
${info.level ? `等级 ${info.level}` : '未知等级'}
${statsHtml}
添加好友
愿望单
`; popup.querySelectorAll('.sf-ui-action-btn').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); const action = btn.dataset.action; if (action === 'addfriend') window.open(`https://steamcommunity.com/profiles/${miniprofile}/friends`, '_blank'); else if (action === 'wishlist') window.open(`https://steamcommunity.com/profiles/${miniprofile}/wishlist`, '_blank'); }); }); }); const closeHandler = e => { if (!popup.contains(e.target) && !element.contains(e.target)) { popup.classList.remove('visible'); setTimeout(() => popup.remove(), 200); document.removeEventListener('click', closeHandler); } }; setTimeout(() => document.addEventListener('click', closeHandler), 100); } function enhanceAuthorInteraction() { $$('.blotter_author_block a[data-miniprofile]').forEach(link => { if (link.dataset.sfEnhanced) return; link.dataset.sfEnhanced = '1'; link.setAttribute('tabindex', '0'); link.setAttribute('role', 'button'); let hoverTimer = null; const open = () => createUserInfoPopup(link.parentElement, link.dataset.miniprofile, link.textContent.trim()); link.addEventListener('mouseenter', () => { hoverTimer = setTimeout(open, 500); }); link.addEventListener('mouseleave', () => { clearTimeout(hoverTimer); hoverTimer = null; }); link.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }); }); } // ==================== 初始化 ==================== function init() { // 注入样式 if (typeof GM_addStyle !== 'undefined') { GM_addStyle(CSS); } else { const style = document.createElement('style'); style.textContent = CSS; document.head.appendChild(style); } // 预加载 API Key 与 SteamID getApiKey(); getMySteamId(); if (isImageViewerPage()) { initImageViewerEnhancement(); } else { waitForElement('#blotter_page', () => { const statusEntry = $('.blotter_status_entry'); const blotterPage = $('#blotter_page'); if (statusEntry && blotterPage) blotterPage.insertBefore(statusEntry, blotterPage.firstChild); createFilterBar(); createSidebarNav(); if (state.layoutMode === 'sidebar') { document.body.classList.add('sf-sidebar-mode'); toggleSidebarVisibility(true); } setTimeout(() => { initLazyLoad(); expandGameCoverImages(); fixGamePurchaseImages(); }, 800); setupLinkTarget(); fixWidth(); }); waitForElement('#friendactivity_right_column', () => { createFriendPanel(); createWaterfallButton(); createLibraryButton(); }); setTimeout(() => { if (!$('#sf-waterfall-btn')) createWaterfallButton(); if (!$('#sf-library-btn')) createLibraryButton(); if (!$('#sf-layout-btn')) createLayoutToggleButton(); }, 2000); setTimeout(() => { observeNewContent(); enhanceAuthorInteraction(); }, 1000); setTimeout(() => setupActivityImageViewer(), 500); const blotterEl = document.getElementById('blotter_content'); if (blotterEl) { const mo = new MutationObserver(fixWidth); mo.observe(blotterEl, { attributes: true, attributeFilter: ['style'] }); registerCleanup(() => mo.disconnect()); } else { waitForElement('#blotter_content', el2 => { const mo = new MutationObserver(fixWidth); mo.observe(el2, { attributes: true, attributeFilter: ['style'] }); registerCleanup(() => mo.disconnect()); }); } } } // 启动 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();