// ==UserScript== // @name Steam 好友动态美化 // @namespace http://tampermonkey.net/ // @version 1.3.5 // @description 美化Steam好友动态页面样式,支持截图/游戏/成就/评测筛选,好友列表悬浮面板,修复游戏封面图显示为SVG的问题 // @author SmallFork // @match https://steamcommunity.com/id/*/home // @match https://steamcommunity.com/id/*/myactivity // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant unsafeWindow // @connect store.steampowered.com // @run-at document-end // @license MIT // @icon data:image/svg+xml, // @tag Steam // @tag games // ==/UserScript== (function() { 'use strict'; const _ver = typeof GM_info !== 'undefined' ? GM_info.script.version : 'unknown'; console.log(`%c[Steam 好友动态美化] v${_ver} 已启动`, 'color:#66c0f4;font-weight:bold;font-size:13px'); /* ========== 页面全局变量访问 ========== */ // 沙箱环境下 window 可能无法访问页面全局变量,每次动态检测 function getPageVar(name) { try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow[name] !== undefined) return unsafeWindow[name]; } catch {} try { if (window[name] !== undefined) return window[name]; } catch {} try { if (document.defaultView && document.defaultView[name] !== undefined) return document.defaultView[name]; } catch {} return undefined; } /* ========== 配置常量 ========== */ const CONFIG = { WAIT_TIMEOUT: 15000, // 等待元素超时(ms) Z_INDEX: 9999, // UI 层级 BTN_SIZE: 30, // 按钮尺寸(px) PANEL_WIDTH: 300, // 好友面板宽度(px) PANEL_MAX_H: '80vh', // 好友面板最大高度 MAX_GAME_SHOW: 12, // 游戏购买动态最大封面数 ANIM_DURATION: 250, // 查看器动画时长(ms) ZOOM_MIN: 0.2, // 最小缩放比例 ZOOM_MAX: 8, // 最大缩放比例 ZOOM_STEP: 0.15, // 缩放步进 DL_IMG_SIZE: 5000 // 下载原图尺寸参数(imw/imh) }; /* ========== 筛选按钮配置 ========== */ const FILTER_BTNS = [ { id: 'sf-screenshot-btn', icon: 'screenshot', title: '只看截图', filter: 'screenshot', gradient: 'var(--sf-green),#3d8b40', shadow: '76,175,80' }, { id: 'sf-game-btn', icon: 'game', title: '只看游戏', filter: 'game', gradient: 'var(--sf-orange),#f57c00', shadow: '255,152,0' }, { id: 'sf-achievement-btn', icon: 'achievement', title: '只看成就', filter: 'achievement', gradient: '#9c27b0,#7b1fa2', shadow: '156,39,176' }, { id: 'sf-review-btn', icon: 'review', title: '只看评测', filter: 'review', gradient: '#e91e63,#c2185b', shadow: '233,30,99' }, { id: 'sf-friend-btn', icon: 'friend', title: '好友列表', filter: null, gradient: 'var(--sf-accent),#4a9bc7', shadow: '102,192,244' } ]; /* ========== 筛选匹配规则 ========== */ const FILTER_MATCHERS = { screenshot: block => !!block.querySelector('.blotter_screenshot'), game: block => !!block.querySelector('.blotter_gamepurchase'), achievement: block => [...block.querySelectorAll('.blotter_daily_rollup_line')] .some(line => { const t = line.textContent || ''; return t.includes('达成了《') || t.includes('的成就'); }), review: block => !!block.querySelector('.blotter_recommendation') }; /* ========== 每日汇总行分类 ========== */ function classifyRollupLine(line) { const t = line.textContent || ''; if (/成为了好友/.test(t)) return 'friend'; if (/第一次玩/.test(t)) return 'firstplay'; if (/添加到.*愿望单/.test(t)) return 'wishlist'; if (/达成了|的成就/.test(t)) return 'achievement'; return null; } function injectRollupStyles() { document.querySelectorAll('.blotter_daily_rollup_line').forEach(line => { if (line.dataset.sfClassified) return; line.dataset.sfClassified = '1'; const cat = classifyRollupLine(line); if (cat) line.classList.add('sf-rollup-' + cat); }); } /* ========== SVG 图标 ========== */ const ICONS = { friend: '', close: '', screenshot: '', game: '', achievement: '', review: '', download: '', top: '', prev: '', next: '' }; /* ========== 工具函数 ========== */ const $ = (sel, ctx = document) => ctx.querySelector(sel); const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)]; const escapeHtml = t => String(t).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); /* ========== 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}`; /* ========== 样式表 ========== */ const CSS = `${CSS_VARS} /* 页面布局 */ .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} /* 页面标题 & 头部 */ .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{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} .blotter_author_block a[data-miniprofile]:hover{color:#fff!important} /* 截图动态 */ .blotter_screenshot{border-radius:0!important;border:none!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:4px!important} /* 游戏状态动态 */ .blotter_userstatus_game{height:auto!important} .blotter_userstatus_game img{height:auto!important} .highlight_strip{overflow-x:auto!important;overflow-y:hidden!important} .highlight_strip_scroll{width:auto!important;display:flex!important;flex-wrap:nowrap!important;overflow-x:auto!important;overflow-y:hidden!important;gap:4px!important;padding:4px!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_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} /* 创意工坊动态 */ .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} /* 评论 & 操作区 */ .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} /* 日期分组 */ .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_appselect_suggestions{background:rgba(23,26,33,.95)!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:8px!important;box-shadow:0 8px 24px rgba(0,0,0,.5)!important;overflow:hidden!important;backdrop-filter:blur(8px)!important;-webkit-backdrop-filter:blur(8px)!important} #blotter_appselect_suggestions .option{color:var(--sf-text)!important;padding:5px 12px!important;font-size:12px!important;border-bottom:1px solid rgba(255,255,255,.04)!important;transition:background .15s ease!important;line-height:1.4!important} #blotter_appselect_suggestions .option:last-child{border-bottom:none!important} #blotter_appselect_suggestions .option:hover{background:rgba(102,192,244,.15)!important;color:#fff!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} /* Steam 原生全屏截图 */ .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} /* Steam 原生弹窗美化 */ #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} /* 右侧按钮组 */ #sf-btn-group{position:fixed!important;bottom:30px!important;right:30px!important;display:flex!important;flex-direction:column!important;gap:10px!important;z-index:${CONFIG.Z_INDEX}!important} #sf-friend-btn,#sf-screenshot-btn,#sf-game-btn,#sf-achievement-btn,#sf-review-btn{width:${CONFIG.BTN_SIZE}px!important;height:${CONFIG.BTN_SIZE}px!important;border-radius:50%!important;border:none!important;cursor:pointer!important;box-shadow:0 4px 20px rgba(0,0,0,.3)!important;transition:var(--sf-transition)!important;display:flex!important;align-items:center!important;justify-content:center!important} #sf-screenshot-btn{background:linear-gradient(135deg,var(--sf-green) 0%,#3d8b40 100%)!important;box-shadow:0 4px 20px rgba(76,175,80,.4)!important} #sf-game-btn{background:linear-gradient(135deg,var(--sf-orange) 0%,#f57c00 100%)!important;box-shadow:0 4px 20px rgba(255,152,0,.4)!important} #sf-achievement-btn{background:linear-gradient(135deg,#9c27b0 0%,#7b1fa2 100%)!important;box-shadow:0 4px 20px rgba(156,39,176,.4)!important} #sf-review-btn{background:linear-gradient(135deg,#e91e63 0%,#c2185b 100%)!important;box-shadow:0 4px 20px rgba(233,30,99,.4)!important} #sf-friend-btn{background:linear-gradient(135deg,var(--sf-accent) 0%,#4a9bc7 100%)!important;box-shadow:0 4px 20px rgba(102,192,244,.4)!important} #sf-friend-btn:hover,#sf-screenshot-btn:hover,#sf-game-btn:hover,#sf-achievement-btn:hover,#sf-review-btn:hover{transform:scale(1.1)!important} #sf-friend-btn svg,#sf-screenshot-btn svg,#sf-game-btn svg,#sf-achievement-btn svg,#sf-review-btn svg{width:28px!important;height:28px!important;fill:#fff!important} /* 筛选按钮激活/未激活状态 */ #sf-btn-group.filtering #sf-screenshot-btn,#sf-btn-group.filtering #sf-game-btn,#sf-btn-group.filtering #sf-achievement-btn,#sf-btn-group.filtering #sf-review-btn{opacity:.35!important;filter:grayscale(.6)!important;transform:scale(.9)!important} #sf-btn-group.filtering #sf-screenshot-btn.active,#sf-btn-group.filtering #sf-game-btn.active,#sf-btn-group.filtering #sf-achievement-btn.active,#sf-btn-group.filtering #sf-review-btn.active{opacity:1!important;filter:none!important;transform:scale(1.1)!important} /* 每日汇总行分类样式 */ .sf-rollup-friend{box-shadow:inset 3px 0 0 #2196F3;background:rgba(33,150,243,.06)!important} .sf-rollup-firstplay{box-shadow:inset 3px 0 0 #4CAF50;background:rgba(76,175,80,.06)!important} .sf-rollup-wishlist{box-shadow:inset 3px 0 0 #FF9800;background:rgba(255,152,0,.06)!important} .sf-rollup-achievement{box-shadow:inset 3px 0 0 #9C27B0;background:rgba(156,39,176,.06)!important} /* 回到顶部 */ #sf-top-btn{width:${CONFIG.BTN_SIZE}px!important;height:${CONFIG.BTN_SIZE}px!important;border-radius:50%!important;border:none!important;cursor:pointer!important;box-shadow:0 4px 20px rgba(0,0,0,.3)!important;transition:var(--sf-transition)!important;display:flex!important;align-items:center!important;justify-content:center!important;background:linear-gradient(135deg,#546e7a 0%,#37474f 100%)!important;opacity:0!important;visibility:hidden!important;transform:scale(.8)!important} #sf-top-btn.show{opacity:1!important;visibility:visible!important;transform:scale(1)!important} #sf-top-btn:hover{transform:scale(1.1)!important;background:linear-gradient(135deg,#607d8b 0%,#455a64 100%)!important} #sf-top-btn svg{width:28px!important;height:28px!important;fill:#fff!important} /* 好友面板 */ #sf-friend-panel{position:fixed!important;bottom:200px!important;right:30px!important;width:${CONFIG.PANEL_WIDTH}px!important;max-height:${CONFIG.PANEL_MAX_H}!important;background:rgba(23,26,33,.95)!important;border-radius:16px!important;border:1px solid rgba(255,255,255,.1)!important;box-shadow:0 8px 40px rgba(0,0,0,.5)!important;z-index:${CONFIG.Z_INDEX - 1}!important;overflow:hidden!important;opacity:0!important;visibility:hidden!important;transform:translateY(20px) scale(.95)!important;transition:var(--sf-transition)!important} #sf-friend-panel.open{opacity:1!important;visibility:visible!important;transform:translateY(0) scale(1)!important} .sf-panel-header{padding:16px!important;background:rgba(0,0,0,.3)!important;border-bottom:1px solid var(--sf-border)!important;display:flex!important;align-items:center!important;justify-content:space-between!important} .sf-panel-title{font-size:16px!important;font-weight:600!important;color:#fff!important} .sf-panel-close{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} .sf-panel-close:hover{background:rgba(255,255,255,.2)!important} .sf-panel-close svg{width:14px!important;height:14px!important;fill:var(--sf-text)!important} .sf-panel-content{padding:12px!important;max-height:calc(70vh - 60px)!important;overflow-y:auto!important} .sf-panel-content::-webkit-scrollbar{width:6px!important} .sf-panel-content::-webkit-scrollbar-thumb{background:rgba(255,255,255,.2)!important;border-radius:3px!important} /* 好友列表项 */ .sf-friend-item{display:flex!important;align-items:center!important;gap:12px!important;padding:10px!important;border-radius:10px!important;background:rgba(255,255,255,.03)!important;margin-bottom:8px!important;transition:all .2s ease!important;cursor:pointer!important} .sf-friend-item:hover{background:var(--sf-hover)!important} .sf-friend-avatar{width:40px!important;height:40px!important;border-radius:8px!important;position:relative!important} .sf-friend-avatar img{width:100%!important;height:100%!important;border-radius:8px!important;object-fit:cover!important} .sf-friend-frame{position:absolute!important;inset:-6px!important;pointer-events:none!important} .sf-friend-frame img{width:100%!important;height:100%!important} .sf-friend-info{flex:1!important;min-width:0!important} .sf-friend-name{font-size:14px!important;font-weight:500!important;color:var(--sf-accent)!important;white-space:nowrap!important;overflow:hidden!important;text-overflow:ellipsis!important} .sf-friend-status{font-size:12px!important;color:var(--sf-dim)!important} .sf-status-dot{width:10px!important;height:10px!important;border-radius:50%!important} .sf-dot-online{background:var(--sf-green)!important} .sf-dot-offline{background:var(--sf-dim)!important} .sf-dot-ingame{background:var(--sf-accent)!important} .sf-own-item{background:rgba(102,192,244,.1)!important;border:1px solid rgba(102,192,244,.2)!important} .sf-action-btn{display:flex!important;align-items:center!important;justify-content:center!important;padding:12px!important;margin-top:8px!important;border-radius:8px!important;background:rgba(255,255,255,.05)!important;color:var(--sf-text)!important;font-size:14px!important;cursor:pointer!important;transition:all .2s ease!important} .sf-action-btn:hover{background:rgba(102,192,244,.15)!important;color:var(--sf-accent)!important} .sf-stats-block{padding:10px!important;margin:8px 0!important;background:rgba(255,255,255,.05)!important;border-radius:8px!important;font-size:13px!important;color:var(--sf-text)!important;text-align:center!important} .sf-section-title{padding:12px 8px 8px!important;font-size:13px!important;font-weight:600!important;color:var(--sf-dim)!important;border-top:1px solid var(--sf-border)!important;margin-top:8px!important} .sf-ingame{color:var(--sf-accent)!important} .sf-view-all{padding:10px!important;text-align:center!important;font-size:12px!important;color:var(--sf-accent)!important;cursor:pointer!important} .sf-view-all:hover{color:#fff!important} .sf-menu-item{display:flex!important;align-items:center!important;padding:8px 12px!important;border-radius:6px!important;background:rgba(255,255,255,.03)!important;color:var(--sf-text)!important;font-size:13px!important;cursor:pointer!important;transition:all .2s ease!important;margin-bottom:4px!important} .sf-menu-item:hover{background:var(--sf-hover)!important;color:var(--sf-accent)!important} /* 移动端适配 */ @media(max-width:600px){.blotter_page_title h1{font-size:20px!important}.friend_activity_header{padding:12px!important}#sf-btn-group{bottom:20px!important;right:20px!important}#sf-friend-btn,#sf-screenshot-btn,#sf-game-btn,#sf-achievement-btn,#sf-review-btn{width:48px!important;height:48px!important}#sf-friend-panel{right:20px!important;width:calc(100vw - 40px)!important;max-width:300px!important}} /* 截图查看器 */ #sf-viewer{position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,.85);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:opacity .35s ease,visibility .35s ease} #sf-viewer.open{opacity:1;visibility:visible} #sf-viewer-img{max-width:90vw;max-height:85vh;object-fit:contain;border-radius:4px;box-shadow:0 8px 40px rgba(0,0,0,.6);cursor:grab;user-select:none;-webkit-user-drag:none} #sf-viewer img:active{cursor:grabbing} #sf-viewer-close{position:fixed;top:20px;right:20px;width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,.12);color:#fff;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s;z-index:100000} #sf-viewer-close:hover{background:rgba(255,255,255,.25)} #sf-viewer-download{position:fixed;bottom:24px;right:24px;padding:10px 20px;border-radius:8px;border:none;background:rgba(102,192,244,.2);color:#66c0f4;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s;z-index:100000;display:flex;align-items:center;gap:8px} #sf-viewer-download:hover{background:rgba(102,192,244,.35);color:#fff} #sf-viewer-zoom{position:fixed;bottom:24px;left:24px;padding:8px 14px;border-radius:8px;background:rgba(0,0,0,.5);color:#8f98a0;font-size:13px;z-index:100000;pointer-events:none} #sf-viewer-hint{position:fixed;bottom:70px;left:50%;transform:translateX(-50%);padding:6px 16px;border-radius:6px;background:rgba(0,0,0,.5);color:#8f98a0;font-size:12px;z-index:100000;pointer-events:none;opacity:0;transition:opacity .3s} #sf-viewer-hint.show{opacity:1} .sf-toast{position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-10px);padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;z-index:100001;opacity:0;transition:all .3s ease;pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.5)} .sf-toast--visible{opacity:1;transform:translateX(-50%) translateY(0)} .sf-toast--info{background:#2a475e;color:#c7d5e0;border:1px solid #4a6a7a} .sf-toast--warn{background:rgba(234,179,8,.9);color:#1a1a1a} /* 切图按钮 */ #sf-viewer-prev,#sf-viewer-next{position:fixed;top:50%;transform:translateY(-50%);width:44px;height:44px;border-radius:50%;border:none;background:rgba(255,255,255,.1);color:#fff;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;z-index:100000;opacity:0;pointer-events:none} #sf-viewer-prev{left:20px} #sf-viewer-next{right:20px} #sf-viewer-prev.show,#sf-viewer-next.show{opacity:1;pointer-events:auto} #sf-viewer-prev:hover,#sf-viewer-next:hover{background:rgba(255,255,255,.25);transform:translateY(-50%) scale(1.1)} /* 图片计数 */ #sf-viewer-counter{position:fixed;top:20px;left:50%;transform:translateX(-50%);padding:6px 14px;border-radius:6px;background:rgba(0,0,0,.5);color:#8f98a0;font-size:13px;z-index:100000;pointer-events:none;display:none} #sf-viewer-counter.show{display:block}`; /* ========== 注入样式 ========== */ if (typeof GM_addStyle !== 'undefined') GM_addStyle(CSS); else { const s = document.createElement('style'); s.textContent = CSS; document.head.appendChild(s); } /* ========== 筛选状态 ========== */ let activeFilter = null; /* ========== 创建 UI ========== */ function createUI() { if ($('#sf-btn-group')) return; const grp = document.createElement('div'); grp.id = 'sf-btn-group'; // 通过配置数组创建筛选按钮 FILTER_BTNS.forEach(({ id, icon, title }) => { const btn = document.createElement('button'); btn.id = id; btn.innerHTML = ICONS[icon]; btn.title = title; grp.appendChild(btn); }); // 回到顶部按钮 const topBtn = document.createElement('button'); topBtn.id = 'sf-top-btn'; topBtn.innerHTML = ICONS.top; topBtn.title = '回到顶部'; grp.appendChild(topBtn); document.body.appendChild(grp); // 回到顶部:滚动显示/隐藏 + 点击 window.addEventListener('scroll', () => { topBtn.classList.toggle('show', window.scrollY > 400); }, { passive: true }); topBtn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' })); // 好友面板 const panel = document.createElement('div'); panel.id = 'sf-friend-panel'; panel.innerHTML = `
好友列表
`; document.body.appendChild(panel); // 事件委托:按钮组点击 grp.addEventListener('click', e => { const btn = e.target.closest('button'); if (!btn) return; e.stopPropagation(); const cfg = FILTER_BTNS.find(b => b.id === btn.id); if (cfg?.filter) { toggleFilter(cfg.filter); } else if (btn.id === 'sf-friend-btn') { if (panel.classList.toggle('open') && !panel.dataset.loaded) { loadFriends(); panel.dataset.loaded = '1'; } } }); // 关闭好友面板 $('.sf-panel-close', panel).addEventListener('click', e => { e.stopPropagation(); panel.classList.remove('open'); }); // 点击面板外关闭 document.addEventListener('click', e => { if (!panel.contains(e.target) && !grp.contains(e.target)) { panel.classList.remove('open'); } }); } /* ========== 筛选切换 ========== */ function toggleFilter(type) { const grp = $('#sf-btn-group'); // 再次点击同一按钮 → 取消筛选,显示全部 if (activeFilter === type) { activeFilter = null; FILTER_BTNS.forEach(b => { if (b.filter) $(`#${b.id}`)?.classList.remove('active'); }); grp.classList.remove('filtering'); showAllBlocks(); return; } // 切换筛选类型 activeFilter = type; FILTER_BTNS.forEach(b => { if (b.filter) $(`#${b.id}`)?.classList.toggle('active', b.filter === type); }); grp.classList.add('filtering'); // 遍历所有动态块,匹配的显示,不匹配的隐藏 const matcher = FILTER_MATCHERS[type]; $$('.blotter_block').forEach(block => { block.classList.toggle('sf-hidden', !matcher(block)); }); } // 显示全部动态块 function showAllBlocks() { $$('.blotter_block.sf-hidden').forEach(b => b.classList.remove('sf-hidden')); } /* ========== 加载好友列表 ========== */ function loadFriends() { const list = $('#sf-friend-list'); const rc = $('#friendactivity_right_column'); if (!list || !rc) { if (list) list.innerHTML = '
暂无数据
'; return; } try { // 自己的信息 const oa = rc.querySelector('.webchat_own_avatar'); const nl = rc.querySelector('.friendslist_entry_content .linkFriend'); const os = rc.querySelector('.friendslist_entry_content'); if (oa && nl) { const ai = oa.querySelector('img[data-miniprofile],img[srcset]'); const fi = oa.querySelector('.profile_avatar_frame img'); const st = os?.querySelector('div')?.textContent || ''; const el = document.createElement('div'); el.className = 'sf-friend-item sf-own-item'; el.innerHTML = `
${escapeHtml(nl.textContent)}${fi ? '
' : ''}
${escapeHtml(nl.textContent)}
${escapeHtml(st)}
`; el.addEventListener('click', () => window.open(nl.href, '_blank')); list.appendChild(el); } // 聊天按钮 const cb = rc.querySelector('.webchat_launch'); if (cb) { const el = document.createElement('div'); el.className = 'sf-action-btn'; el.innerHTML = '💬 发送聊天消息'; el.addEventListener('click', () => typeof window.LaunchWebChat === 'function' && window.LaunchWebChat()); list.appendChild(el); } // 好友统计 const fs = rc.querySelector('.profile_link_block_text_friends'); if (fs) { const el = document.createElement('div'); el.className = 'sf-stats-block'; el.textContent = fs.textContent; list.appendChild(el); } // 查看好友列表 const vl = rc.querySelector('.friends_link_block'); if (vl) { const lk = vl.closest('a') || rc.querySelector('a[href*="/friends"]'); const el = document.createElement('div'); el.className = 'sf-action-btn'; el.innerHTML = '👥 查看好友列表'; el.addEventListener('click', () => window.open(lk?.href || 'https://steamcommunity.com/my/friends', '_blank')); list.appendChild(el); } // 游戏中的好友 const ifs = rc.querySelectorAll('.friendBlock.in-game'); if (ifs.length) { const tt = rc.querySelector('.profile_friends_ingame'); const title = document.createElement('div'); title.className = 'sf-section-title'; title.textContent = tt?.textContent || '游戏中的好友'; list.appendChild(title); ifs.forEach(f => { const av = f.querySelector('.playerAvatar img'); const lk = f.querySelector('.friendBlockLinkOverlay'); const gm = f.querySelector('.linkFriend_in-game'); // 多重兜底解析好友名 let n = f.querySelector('.friendBlockContent')?.firstChild?.textContent?.trim() || f.querySelector('.friendBlockContent a')?.textContent?.trim() || f.querySelector('.friendBlockContent')?.textContent?.split('\n')[0]?.trim() || lk?.getAttribute('href')?.split('/').pop()?.replace(/id\/|profiles\//, '') || '未知'; const g = gm?.textContent?.replace(/游戏中\n|非 Steam 游戏中\n/g, '') || ''; const el = document.createElement('div'); el.className = 'sf-friend-item'; el.innerHTML = `
${escapeHtml(n)}
${escapeHtml(n)}
🎮 ${escapeHtml(g)}
`; el.addEventListener('click', () => window.open(lk?.href || '', '_blank')); list.appendChild(el); }); // 查看全部 const va = rc.querySelector('.profile_friends_ingame_viewall a'); if (va) { const el = document.createElement('div'); el.className = 'sf-view-all'; el.textContent = va.textContent; el.addEventListener('click', () => window.open(va.href, '_blank')); list.appendChild(el); } } // 快捷菜单 const mi = rc.querySelectorAll('.profile_menu_item a'); if (mi.length) { const title = document.createElement('div'); title.className = 'sf-section-title'; title.textContent = '快捷菜单'; list.appendChild(title); mi.forEach(a => { const t = a.querySelector('.profile_menu_text')?.textContent?.trim() || ''; const el = document.createElement('div'); el.className = 'sf-menu-item'; el.innerHTML = `${escapeHtml(t)}`; el.addEventListener('click', () => window.open(a.href, '_blank')); list.appendChild(el); }); } } catch (err) { console.warn('[SF] 好友列表加载失败:', err); list.innerHTML = '
加载失败
'; } } /* ========== 修复宽度(移除 Steam 内联 style) ========== */ function fixWidth() { const el = document.getElementById('blotter_content'); if (el?.style.width) el.removeAttribute('style'); // 清除截图画廊滚动区域的内联 width $$('.highlight_strip_scroll').forEach(s => { if (s.style.width) s.style.removeProperty('width'); }); } /* ========== 展开游戏购买动态的图片(默认3张→8张) ========== */ const CAPSULE_URL = 'https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/capsule_231x87.jpg'; const CDN_BASE = 'https://cdn.cloudflare.steamstatic.com/steam/apps/'; 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`; // 封面图候选尺寸列表,依次尝试,全部失败则通过 API 获取 const FALLBACK_CAPSULE_PATHS = [ 'capsule_231x87.jpg', 'capsule_231x87.png', 'header.jpg', 'capsule_184x69.jpg', 'capsule_467x181.jpg' ]; // API 缓存:appid -> capsule URL,避免重复请求 const _capsuleCache = new Map(); // 正在请求中的 appid 集合,避免并发重复请求 const _capsulePending = new Set(); // 等待某个 appid API 结果的回调队列 const _capsuleWaiters = new Map(); /* 通过 Steam Store API 获取真实封面图 URL */ 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}`; const doRequest = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : null; if (doRequest) { doRequest({ method: 'GET', url: apiUrl, onload: (resp) => { try { const json = JSON.parse(resp.responseText); const data = json[appid]; if (data && data.success && data.data && data.data.capsule_image) { const url = data.data.capsule_image; _capsuleCache.set(appid, url); } else { _capsuleCache.set(appid, ''); } } catch (e) { _capsuleCache.set(appid, ''); } _capsulePending.delete(appid); const waiters = _capsuleWaiters.get(appid) || []; _capsuleWaiters.delete(appid); const cached = _capsuleCache.get(appid); waiters.forEach(w => cached ? w.resolve(cached) : w.reject()); }, onerror: () => { _capsuleCache.set(appid, ''); _capsulePending.delete(appid); const waiters = _capsuleWaiters.get(appid) || []; _capsuleWaiters.delete(appid); waiters.forEach(w => w.reject()); }, ontimeout: () => { _capsuleCache.set(appid, ''); _capsulePending.delete(appid); const waiters = _capsuleWaiters.get(appid) || []; _capsuleWaiters.delete(appid); waiters.forEach(w => w.reject()); } }); } else { // 降级:使用 fetch(可能受 CORS 限制) 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); const waiters = _capsuleWaiters.get(appid) || []; _capsuleWaiters.delete(appid); waiters.forEach(w => url ? w.resolve(url) : w.reject()); }) .catch(() => { _capsuleCache.set(appid, ''); _capsulePending.delete(appid); const waiters = _capsuleWaiters.get(appid) || []; _capsuleWaiters.delete(appid); waiters.forEach(w => w.reject()); }); } }); } /* 为 img 元素依次尝试多个 CDN 封面地址,全部失败后通过 API 获取真实 URL */ function loadCapsuleWithFallback(img, appid) { let idx = 0; function tryNext() { if (idx >= FALLBACK_CAPSULE_PATHS.length) { // CDN 路径全部失败,通过 Steam API 获取真实封面 fetchCapsuleFromAPI(appid).then(url => { if (url) { img.onerror = () => { img.onerror = null; img.src = SVG_PLACEHOLDER; }; img.src = url; } else { img.onerror = null; img.src = SVG_PLACEHOLDER; } }).catch(() => { img.onerror = null; img.src = SVG_PLACEHOLDER; }); return; } img.onerror = () => { idx++; tryNext(); }; img.src = CDN_BASE + appid + '/' + FALLBACK_CAPSULE_PATHS[idx]; } tryNext(); } /* ========== 修复已有游戏购买封面的 SVG / 破损图片 ========== */ function fixGamePurchaseImages() { $$('.blotter_gamepurchase_logo').forEach(logo => { if (logo.dataset.sfImgFixed) return; logo.dataset.sfImgFixed = '1'; const appid = extractAppidFromContext(logo); if (!appid) return; // 找不到 appid,无法修复 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) { // 存在但 src 是 SVG/空 → 用回退链替换 loadCapsuleWithFallback(img, appid); } else { // 看起来正常,添加 onerror 兜底(不主动覆盖原 src) img.addEventListener('error', () => loadCapsuleWithFallback(img, appid), { once: true }); } } else if (inlineSvg) { // logo 内没有 ,而是内联 → 移除 SVG,创建 加载真实封面 inlineSvg.remove(); const newImg = document.createElement('img'); newImg.alt = `Game #${appid}`; newImg.loading = 'lazy'; logo.appendChild(newImg); loadCapsuleWithFallback(newImg, appid); } }); } /* 从 logo 元素或其所在购买块的上下文中提取 appid(多重回退) */ function extractAppidFromContext(logoElement) { // 1) 自身 data-appid let m; const dataAppid = logoElement.getAttribute('data-appid'); if (dataAppid) return dataAppid; // 2) 自身 href m = (logoElement.getAttribute('href') || '').match(/\/app\/(\d+)/); if (m) return m[1]; // 3) 内部 href const innerA = logoElement.querySelector('a[href*="/app/"]'); if (innerA) { m = (innerA.getAttribute('href') || '').match(/\/app\/(\d+)/); if (m) return m[1]; } // 4) 从同购买块的 details 链接匹配(排除已被其他 logo 使用的 appid) 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 expandGameImages() { $$('.blotter_gamepurchase').forEach(block => { if (block.dataset.sfExpanded) return; block.dataset.sfExpanded = '1'; const content = block.querySelector('.blotter_gamepurchase_content'); const details = block.querySelector('.blotter_gamepurchase_details'); const plusBlock = content?.querySelector('.plus_block'); if (!content || !details || !plusBlock) return; const existingLogos = content.querySelectorAll('.blotter_gamepurchase_logo'); if (existingLogos.length >= CONFIG.MAX_GAME_SHOW) return; // 从 details 链接提取 appid 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 >= CONFIG.MAX_GAME_SHOW) return; const match = (link.getAttribute('href') || '').match(/\/app\/(\d+)/); if (!match || existingAppids.has(match[1])) return; const a = document.createElement('a'); a.href = link.href; a.className = 'blotter_gamepurchase_logo es_ea_checked'; a.setAttribute('data-appid', match[1]); const img = document.createElement('img'); img.loading = 'lazy'; img.alt = `Game #${match[1]}`; // 使用多格式回退链加载封面,避免直接显示 SVG 占位符 loadCapsuleWithFallback(img, match[1]); a.appendChild(img); content.insertBefore(a, plusBlock); added++; }); // 更新 +N 数字 const totalShown = existingLogos.length + added; totalShown < links.length ? (plusBlock.textContent = '+' + (links.length - totalShown)) : 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')); // 监听新增节点 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')); } } }).observe(blotter, { childList: true, subtree: true }); } /* ========== 自定义截图查看器 ========== */ function setupScreenshotViewer() { // 创建查看器 DOM const viewer = document.createElement('div'); viewer.id = 'sf-viewer'; viewer.innerHTML = `
100%
滚轮缩放 · 拖拽移动 · ESC关闭
`; document.body.appendChild(viewer); const img = $('#sf-viewer-img'); const closeBtn = $('#sf-viewer-close'); const prevBtn = $('#sf-viewer-prev'); const nextBtn = $('#sf-viewer-next'); const downloadBtn = $('#sf-viewer-download'); const zoomLabel = $('#sf-viewer-zoom'); const counter = $('#sf-viewer-counter'); const hint = $('#sf-viewer-hint'); let scale = 1, offsetX = 0, offsetY = 0; let dragging = false, dragStartX, dragStartY, startOffsetX, startOffsetY; // 画廊数据:shots 数组 + 当前索引 let galleryShots = []; let galleryIndex = 0; // 当前截图的原图下载地址 let downloadUrl = ''; // 原始图片引用(用于 FLIP 关闭动画) let sourceImg = null; let isAnimating = false; // 已加载原图的 shot ID 集合(跨开闭保持) const loadedOriginals = new Set(); // JS rAF 逐帧动画(IT之家风格:直接动画 width/height/left/top) function animateRect(el, from, to, duration, onDone) { const startTime = performance.now(); const props = ['width', 'height', 'left', 'top']; const doOpacity = from.opacity !== undefined && to.opacity !== undefined; function tick(now) { let t = Math.min((now - startTime) / duration, 1); props.forEach(p => { el.style[p] = (from[p] + (to[p] - from[p]) * t) + 'px'; }); if (doOpacity) el.style.opacity = from.opacity + (to.opacity - from.opacity) * t; if (t < 1) requestAnimationFrame(tick); else if (onDone) onDone(); } requestAnimationFrame(tick); } // 从 baseurl 构建原图下载 URL function getDownloadUrl(baseurl, src) { if (baseurl) return baseurl + `?imw=${CONFIG.DL_IMG_SIZE}&imh=${CONFIG.DL_IMG_SIZE}&letterbox=false`; // 降级:修改 CDN 参数获取原图 if (!src) return ''; try { const url = new URL(src); url.searchParams.set('imw', String(CONFIG.DL_IMG_SIZE)); url.searchParams.set('imh', String(CONFIG.DL_IMG_SIZE)); url.searchParams.set('letterbox', 'false'); return url.toString(); } catch { return src; } } // 获取 shot 的唯一标识(用于 loadedOriginals) function getShotKey(shot) { return shot.m_id || shot.m_baseurl || shot.m_img || ''; } // 获取显示用的图片 src:已加载原图则返回原图 URL,否则缩略图 function getDisplaySrc(shot) { if (loadedOriginals.has(getShotKey(shot))) { return getDownloadUrl(shot.m_baseurl, shot.m_img || ''); } return shot.m_img || ''; } // 更新"加载原图"按钮状态 function updateDownloadBtn() { const shot = galleryShots[galleryIndex]; if (!shot) return; if (loadedOriginals.has(getShotKey(shot))) { downloadBtn.innerHTML = `${ICONS.download}已加载`; } else { downloadBtn.innerHTML = `${ICONS.download}加载原图`; } } // 更新切图按钮显示 function updateNavButtons() { const hasMultiple = galleryShots.length > 1; prevBtn.classList.toggle('show', hasMultiple); nextBtn.classList.toggle('show', hasMultiple); counter.classList.toggle('show', hasMultiple); if (hasMultiple) { counter.textContent = `${galleryIndex + 1} / ${galleryShots.length}`; } } // 切换到指定截图 function navigateTo(index) { if (index < 0 || index >= galleryShots.length) return; galleryIndex = index; const shot = galleryShots[index]; const displaySrc = getDisplaySrc(shot); downloadUrl = getDownloadUrl(shot.m_baseurl, shot.m_img || ''); // 切图后恢复原图 opacity 并清除 FLIP 回移动画 if (sourceImg) { sourceImg.style.opacity = ''; sourceImg = null; } // 重置缩放和位移,清除动画残留的 inline 样式 scale = 1; offsetX = 0; offsetY = 0; img.style.position = ''; img.style.margin = ''; img.style.maxWidth = ''; img.style.maxHeight = ''; img.style.width = ''; img.style.height = ''; img.style.left = ''; img.style.top = ''; img.style.borderRadius = ''; img.style.transform = ''; img.style.opacity = ''; zoomLabel.textContent = '100%'; img.src = displaySrc; updateNavButtons(); updateDownloadBtn(); } // 显示查看器(JS rAF 逐帧动画,IT之家风格) function openViewer(src, galleryId, shotId, clickedEl) { // 尝试从 Steam 全局变量获取画廊数据 galleryShots = []; galleryIndex = 0; const g_BlotterGalleries = getPageVar('g_BlotterGalleries'); if (galleryId && g_BlotterGalleries && g_BlotterGalleries[galleryId]) { const gallery = g_BlotterGalleries[galleryId]; const shotKeys = Object.keys(gallery.shots || {}); if (shotKeys.length > 0) { galleryShots = shotKeys.map(k => gallery.shots[k]); if (shotId) { const idx = galleryShots.findIndex(s => s.m_id === shotId); if (idx >= 0) galleryIndex = idx; } } } // 单图降级处理 if (galleryShots.length === 0) { galleryShots = [{ m_img: src, m_baseurl: '' }]; galleryIndex = 0; } const shot = galleryShots[galleryIndex]; const displaySrc = getDisplaySrc(shot); downloadUrl = getDownloadUrl(shot.m_baseurl, shot.m_img || ''); scale = 1; offsetX = 0; offsetY = 0; // 保存原始图片引用,隐藏原图(避免视觉重复) sourceImg = clickedEl || null; if (sourceImg) sourceImg.style.opacity = '0'; const sourceRect = clickedEl?.getBoundingClientRect(); // 先隐藏 img,显示 viewer 背景 img.style.opacity = '0'; img.style.transform = ''; zoomLabel.textContent = '100%'; viewer.classList.add('open'); document.body.style.overflow = 'hidden'; // 等图片加载后再启动动画 function startOpenAnimation() { const targetRect = img.getBoundingClientRect(); if (sourceRect && targetRect.width > 0 && targetRect.height > 0) { // 切换到 fixed 定位,放在缩略图位置 img.style.position = 'fixed'; img.style.margin = '0'; img.style.maxWidth = 'none'; img.style.maxHeight = 'none'; img.style.width = sourceRect.width + 'px'; img.style.height = sourceRect.height + 'px'; img.style.left = sourceRect.left + 'px'; img.style.top = sourceRect.top + 'px'; img.style.opacity = '1'; img.style.borderRadius = '0'; isAnimating = true; // 下一帧启动逐帧动画 requestAnimationFrame(() => { animateRect(img, { width: sourceRect.width, height: sourceRect.height, left: sourceRect.left, top: sourceRect.top }, { width: targetRect.width, height: targetRect.height, left: targetRect.left, top: targetRect.top }, CONFIG.ANIM_DURATION, () => { // 动画完成,恢复 flex 居中布局 img.style.position = ''; img.style.margin = ''; img.style.maxWidth = ''; img.style.maxHeight = ''; img.style.width = ''; img.style.height = ''; img.style.left = ''; img.style.top = ''; img.style.borderRadius = ''; isAnimating = false; }); }); } else { img.style.opacity = '1'; } } img.onload = () => { img.onload = null; requestAnimationFrame(startOpenAnimation); }; img.src = displaySrc; // 图片已缓存时直接触发 if (img.complete && img.naturalWidth > 0) { img.onload = null; requestAnimationFrame(startOpenAnimation); } updateNavButtons(); updateDownloadBtn(); // 简短提示 setTimeout(() => hint.classList.add('show'), 300); setTimeout(() => hint.classList.remove('show'), 3000); } // 关闭查看器(JS rAF 逐帧回移动画) function closeViewer() { const currentSource = sourceImg; const sourceRect = currentSource?.getBoundingClientRect(); if (sourceRect && !isAnimating) { const currentRect = img.getBoundingClientRect(); if (currentRect.width > 0 && currentRect.height > 0) { // 重置缩放/拖拽 scale = 1; offsetX = 0; offsetY = 0; // 切换到 fixed 定位在当前位置 img.style.position = 'fixed'; img.style.margin = '0'; img.style.maxWidth = 'none'; img.style.maxHeight = 'none'; img.style.width = currentRect.width + 'px'; img.style.height = currentRect.height + 'px'; img.style.left = currentRect.left + 'px'; img.style.top = currentRect.top + 'px'; img.style.transform = ''; img.style.borderRadius = '0'; isAnimating = true; // 背景同步淡出 viewer.classList.remove('open'); requestAnimationFrame(() => { animateRect(img, { width: currentRect.width, height: currentRect.height, left: currentRect.left, top: currentRect.top, opacity: 1 }, { width: sourceRect.width, height: sourceRect.height, left: sourceRect.left, top: sourceRect.top, opacity: 0 }, CONFIG.ANIM_DURATION, () => { cleanup(); }); }); // 兜底 setTimeout(() => cleanup(), 300); return; } } // 无动画直接关闭 cleanup(); function cleanup() { if (cleanup.done) return; cleanup.done = true; viewer.classList.remove('open'); document.body.style.overflow = ''; img.style.position = ''; img.style.margin = ''; img.style.maxWidth = ''; img.style.maxHeight = ''; img.style.width = ''; img.style.height = ''; img.style.left = ''; img.style.top = ''; img.style.borderRadius = ''; img.style.transform = ''; img.style.opacity = ''; img.src = ''; downloadUrl = ''; if (sourceImg) { sourceImg.style.opacity = ''; sourceImg = null; } isAnimating = false; galleryShots = []; galleryIndex = 0; prevBtn.classList.remove('show'); nextBtn.classList.remove('show'); counter.classList.remove('show'); } } function updateTransform() { img.style.transform = `scale(${scale}) translate(${offsetX}px, ${offsetY}px)`; } // 滚轮缩放 viewer.addEventListener('wheel', e => { if (!viewer.classList.contains('open')) return; e.preventDefault(); scale = Math.min(CONFIG.ZOOM_MAX, Math.max(CONFIG.ZOOM_MIN, scale + (e.deltaY > 0 ? -CONFIG.ZOOM_STEP : CONFIG.ZOOM_STEP))); updateTransform(); zoomLabel.textContent = Math.round(scale * 100) + '%'; }, { passive: false }); // 拖拽移动 img.addEventListener('mousedown', e => { if (scale <= 1) return; e.preventDefault(); dragging = true; dragStartX = e.clientX; dragStartY = e.clientY; startOffsetX = offsetX; startOffsetY = offsetY; }); document.addEventListener('mousemove', e => { if (!dragging) return; offsetX = startOffsetX + (e.clientX - dragStartX) / scale; offsetY = startOffsetY + (e.clientY - dragStartY) / scale; updateTransform(); }); document.addEventListener('mouseup', () => { dragging = false; }); // 关闭按钮 closeBtn.addEventListener('click', closeViewer); // 点击背景关闭(排除按钮和图片) viewer.addEventListener('click', e => { if (e.target === viewer) closeViewer(); }); // ESC 关闭 + 左右箭头切图 document.addEventListener('keydown', e => { if (!viewer.classList.contains('open')) return; if (e.key === 'Escape') closeViewer(); else if (e.key === 'ArrowLeft') { if (galleryIndex <= 0) { showToast('已经是第一张了', 'warn'); return; } navigateTo(galleryIndex - 1); } else if (e.key === 'ArrowRight') { if (galleryIndex >= galleryShots.length - 1) { showToast('已经是最后一张了', 'warn'); return; } navigateTo(galleryIndex + 1); } }); // Toast 通知(参考许可分类器) let toastTimer = null; let currentToast = null; function showToast(msg, type = 'info', ms = 1500) { // 先移除上一个 toast if (currentToast) { currentToast.remove(); currentToast = null; } clearTimeout(toastTimer); const el = document.createElement('div'); el.className = `sf-toast sf-toast--${type}`; el.textContent = msg; document.body.appendChild(el); currentToast = el; requestAnimationFrame(() => requestAnimationFrame(() => el.classList.add('sf-toast--visible'))); toastTimer = setTimeout(() => { el.classList.remove('sf-toast--visible'); setTimeout(() => { el.remove(); if (currentToast === el) currentToast = null; }, 300); }, ms); } // 切图按钮 prevBtn.addEventListener('click', e => { e.stopPropagation(); if (galleryIndex <= 0) { showToast('已经是第一张了', 'warn'); return; } navigateTo(galleryIndex - 1); }); nextBtn.addEventListener('click', e => { e.stopPropagation(); if (galleryIndex >= galleryShots.length - 1) { showToast('已经是最后一张了', 'warn'); return; } navigateTo(galleryIndex + 1); }); // 加载原图(替换当前缩略图为高清原图) downloadBtn.addEventListener('click', () => { const shot = galleryShots[galleryIndex]; if (!shot) return; const shotKey = getShotKey(shot); if (loadedOriginals.has(shotKey)) return; // 已加载,忽略 const url = downloadUrl || img.src; if (!url) return; downloadBtn.style.opacity = '0.6'; downloadBtn.textContent = '加载中...'; const hdImg = new Image(); hdImg.onload = () => { loadedOriginals.add(shotKey); img.src = url; downloadBtn.style.opacity = '1'; downloadBtn.innerHTML = `${ICONS.download}已加载`; }; hdImg.onerror = () => { downloadBtn.style.opacity = '1'; downloadBtn.innerHTML = `${ICONS.download}加载原图`; }; hdImg.src = url; }); // 拦截截图点击,使用事件委托(捕获阶段优先于 Steam 的处理) const blotter = document.getElementById('blotter_content'); if (!blotter) return; blotter.addEventListener('click', e => { const screenshotLink = e.target.closest('.blotter_screenshot_image a, .blotter_screenshot_gallery_image a, .blotter_screenshot a'); if (!screenshotLink) return; const clickedImg = screenshotLink.querySelector('img') || e.target.closest('img'); if (!clickedImg) return; // 阻止 Steam 默认弹窗 e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const src = clickedImg.srcset?.split(' ')[0] || clickedImg.src; // 获取画廊 ID const screenshotBlock = clickedImg.closest('.blotter_screenshot'); const galleryId = screenshotBlock?.querySelector('.blotter_screenshot_image')?.id || screenshotBlock?.querySelector('.highlight_strip_scroll')?.dataset.galleryid || ''; // 获取当前截图 ID // 1) 优先从 Steam 全局变量获取当前激活的截图 // 2) 其次从缩略图 id 提取(格式:gallery_XXX_shotId) // 3) 最后通过 src URL 匹配 let shotId = ''; const g_BlotterGalleries = getPageVar('g_BlotterGalleries'); if (galleryId && g_BlotterGalleries && 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]; } if (src) openViewer(src, galleryId, shotId, clickedImg); }, true); } /* ========== 等待元素出现 ========== */ function wait(sel, cb) { const el = $(sel); if (el) { cb(el); return; } const obs = new MutationObserver(() => { const found = $(sel); if (found) { obs.disconnect(); cb(found); } }); obs.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { obs.disconnect(); createUI(); fixWidth(); }, CONFIG.WAIT_TIMEOUT); } /* ========== 初始化 ========== */ wait('#friendactivity_right_column', () => { createUI(); fixWidth(); expandGameImages(); fixGamePurchaseImages(); setupLinkTarget(); setupScreenshotViewer(); injectRollupStyles(); // 监听异步加载的新动态,自动展开游戏封面 + 重新筛选 + 注入分类标签 const blotterContent = document.getElementById('blotter_content'); if (blotterContent) { new MutationObserver(mutations => { let needExpand = false, needFilter = false, needBadge = false; for (const m of mutations) { for (const node of m.addedNodes) { if (node.nodeType !== 1) continue; if (node.querySelector?.('.blotter_gamepurchase') || node.classList?.contains('blotter_gamepurchase')) needExpand = true; if (node.querySelector?.('.blotter_block') || node.classList?.contains('blotter_block')) needFilter = true; if (node.querySelector?.('.blotter_daily_rollup') || node.classList?.contains('blotter_daily_rollup')) needBadge = true; } } if (needExpand) expandGameImages(); if (needExpand) fixGamePurchaseImages(); if (needBadge) injectRollupStyles(); if (needFilter && activeFilter) { const matcher = FILTER_MATCHERS[activeFilter]; $$('.blotter_block').forEach(block => { block.classList.toggle('sf-hidden', !matcher(block)); }); } }).observe(blotterContent, { childList: true, subtree: true }); } }); // 观察 #blotter_content 的 style 属性变化,自动修复宽度 const blotterEl = document.getElementById('blotter_content'); if (blotterEl) { new MutationObserver(fixWidth).observe(blotterEl, { attributes: true, attributeFilter: ['style'] }); } else { wait('#blotter_content', el => { new MutationObserver(fixWidth).observe(el, { attributes: true, attributeFilter: ['style'] }); }); } })();