/* eslint-disable max-len */ // ==UserScript== // @name PSN中文网功能增强 // @namespace https://swsoyee.github.io // @version 1.0.35 // @description 数折价格走势图,显示人民币价格,奖杯统计和筛选,发帖字数统计和即时预览,楼主高亮,自动翻页,屏蔽黑名单用户发言,被@用户的发言内容显示等多项功能优化P9体验 // eslint-disable-next-line max-len // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAMAAAAp4XiDAAAAMFBMVEVHcEw0mNs0mNs0mNs0mNs0mNs0mNs0mNs0mNs0mNs0mNs0mNs0mNs0mNs0mNs0mNuEOyNSAAAAD3RSTlMAQMAQ4PCApCBQcDBg0JD74B98AAABN0lEQVRIx+2WQRaDIAxECSACWLn/bdsCIkNQ2XXT2bTyHEx+glGIv4STU3KNRccp6dNh4qTM4VDLrGVRxbLGaa3ZQSVQulVJl5JFlh3cLdNyk/xe2IXz4DqYLhZ4mWtHd4/SLY/QQwKmWmGcmUfHb4O1mu8BIPGw4Hg1TEvySQGWoBcItgxndmsbhtJd6baukIKnt525W4anygNECVc1UD8uVbRNbumZNl6UmkagHeRJfX0BdM5NXgA+ZKESpiJ9tRFftZEvue2cS6cKOrGk/IOLTLUcaXuZHrZDq3FB2IonOBCHIy8Bs1Zzo1MxVH+m8fQ+nFeCQM3MWwEsWsy8e8Di7meA5Bb5MDYCt4SnUbP3lv1xOuWuOi3j5kJ5tPiZKahbi54anNRaaG7YElFKQBHR/9PjN3oD6fkt9WKF9rgAAAAASUVORK5CYII= // @author InfinityLoop, mordom0404, Nathaniel_Wu, JayusTree // @include *psnine.com/* // @include *d7vg.com/* // @require http://libs.baidu.com/jquery/2.1.4/jquery.min.js // @require https://code.highcharts.com/11.1.0/highcharts.js // @require https://code.highcharts.com/11.1.0/modules/histogram-bellcurve.js // @require https://unpkg.com/tippy.js@3/dist/tippy.all.min.js // @license MIT // @supportURL https://github.com/swsoyee/psnine-night-mode-CSS/issues/new // @compatible chrome // @compatible firefox // @compatible edge // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/375985/PSN%E4%B8%AD%E6%96%87%E7%BD%91%E5%8A%9F%E8%83%BD%E5%A2%9E%E5%BC%BA.user.js // @updateURL https://update.greasyfork.icu/scripts/375985/PSN%E4%B8%AD%E6%96%87%E7%BD%91%E5%8A%9F%E8%83%BD%E5%A2%9E%E5%BC%BA.meta.js // ==/UserScript== /* globals $, Highcharts, tippy, GM_getValue, GM_setValue */ (function () { const settings = { // 功能0-3设置:鼠标滑过黑条即可显示内容 hoverUnmark: true, // 设置为false则选中才显示 // 功能0-5设置:是否开启自动签到 autoCheckIn: true, // 功能0-6: 自动翻页 autoPaging: 0, // 自动往后翻的页数 // 功能0-7:个人主页下显示所有游戏 autoPagingInHomepage: true, // 功能1-4:回复内容回溯 replyTraceback: true, // 功能1-1设置:高亮发帖楼主功能 highlightBack: '#3890ff', // 高亮背景色 highlightFront: '#ffffff', // 高亮字体颜色 // 功能1-2设置:高亮具体ID功能(默认管理员id) // 注:此部分功能源于@mordom0404的P9工具包: // https://greasyfork.org/zh-CN/scripts/29343-p9%E5%B7%A5%E5%85%B7%E5%8C%85 highlightSpecificID: ['mechille', 'sai8808', 'jimmyleo', 'jimmyleohk', 'monica_zjl', 'yinssk'], // 需要高亮的ID数组 highlightSpecificBack: '#d9534f', // 高亮背景色 highlightSpecificFront: '#ffffff', // 高亮字体颜色 // 功能1-6设置:屏蔽黑名单中的用户发言内容 blockList: [], // 请在左侧输入用户ID,用逗号进行分割,如: ['use_a', 'user_b', 'user_c'] 以此类推 // 屏蔽词, blockWordsList: [], // 问答页面状态UI优化 newQaStatus: true, // 功能1-11设置:鼠标悬浮于头像显示个人奖杯卡 hoverHomepage: true, // 功能4-3设置:汇总以获得和未获得奖杯是否默认折叠 foldTrophySummary: false, // true则默认折叠,false则默认展开 // 功能5-1设置:是否在`游戏`页面启用降低无白金游戏的图标透明度 filterNonePlatinumAlpha: 0.2, // 透密 [0, 1] 不透明,如果设置为1则关闭该功能 // 设置热门标签阈值 hotTagThreshold: 20, // 夜间模式 nightMode: false, // 自动夜间模式 autoNightMode: { value: 'SYSTEM', enum: ['SYSTEM', 'TIME', 'OFF'], // options in settings panel have to be in the same order }, // 约战页面去掉发起人头像 removeHeaderInBattle: false, // 机因、问答页面按最新排序 listPostsByNew: false, // 载入全部问答答案 showAllQAAnswers: false, // 答案按最新排列 listQAAnswersByNew: false, // 答案显示隐藏回复 showHiddenQASubReply: false, // 检测纯文本中的链接 fixTextLinks: true, // 修复D7VG链接 fixD7VGLinks: true, // 站内使用HTTPS链接 fixHTTPLinks: false, // 尝试关联不同版本的游戏 referGameVariants: true, // 查询游戏版本优先使用搜索 preferSearchForFindingVariants: false, // 展开隐藏的子评论 expandCollapsedSubcomments: true, // 约战页面显示相关游戏个人游戏进度 showGameProgressInBattle: true, // 约战缓存更新时间 BattleInfoUpdateInterval: 60 * 60 * 1000, }; if (window.localStorage) { if (window.localStorage['psnine-night-mode-CSS-settings']) { const localSettings = JSON.parse(window.localStorage['psnine-night-mode-CSS-settings']); let settingTypeUpdated = false; Object.keys(settings).forEach((key) => { if (typeof settings[key] !== typeof localSettings[key]) { localSettings[key] = settings[key]; settingTypeUpdated = true; } }); $.extend(settings, localSettings); // 用storage中的配置项覆盖默认设置 if (settingTypeUpdated) localStorage['psnine-night-mode-CSS-settings'] = JSON.stringify(localSettings); } } else { console.log('浏览器不支持localStorage,使用默认配置项'); } // 获取自己的PSN ID const psnidCookie = document.cookie.match(/__Psnine_psnid=(\w+);/); // 全局优化 function onDocumentStart() { // run before anything is downloaded // 站内使用HTTPS链接 if (settings.fixHTTPLinks && /^http:\/\//.test(window.location.href)) window.location.href = window.location.href.replace('http://', 'https://'); // 机因、问答页面按最新排序 if (settings.listPostsByNew && /\/((gene)|(qa))($|(\/$))/.test(window.location.href)) { window.location.href += '?ob=date'; } // 功能0-2:夜间模式 const toggleNightMode = () => { if (settings.nightMode) { const node = document.createElement('style'); node.id = 'nightModeStyle'; node.type = 'text/css'; node.appendChild(document.createTextNode(` li[style="background:#f5faec"]{background:#344836 !important;}li[style="background:#fdf7f7"]{background:#4f3945 !important;}li[style="background:#faf8f0"]{background:#4e4c39 !important;}li[style="background:#f4f8fa"]{background:#505050 !important;}span[style="color:blue;"]{color:#64a5ff !important;}span[style="color:red;"],span[style="color:#a10000"]{color:#ff6464 !important;}span[style="color:brown;"]{color:#ff8864 !important;}.tit3{color:white !important;}.mark{background:#bbb !important;color:#bbb;}body.bg{background:#2b2b2b !important;}.list li,.box .post,td,th{border-bottom:1px solid #555;}.list li:nth-last-child(1),th:nth-last-child(1){border-bottom:none;}.psnnode{background:#656565;}.box{background:#3d3d3d !important;}.title a{color:#bbb;}.text-strong,strong,.storeinfo,.content{color:#bbb !important;}.alert-warning,.alert-error,.alert-success,.alert-info{background:#4b4b4b !important;}.alert-error{color:#ec6666;}.text-error{color:#ec6666 !important;}h1,.title2{color:#ffffff !important;}.twoge{color:#ffffff !important;}.inav{background:#3d3d3d !important;}.inav li.current{background:#4b4b4b !important;}.ml100 p{color:#ffffff !important;}.t1{background:#657caf !important;}.t2{background:#845e2f !important;}.t3{background:#707070 !important;}.t4{background:#8b4d2d !important;}blockquote{background:#bababa !important;}.text-gray{color:#bbb !important;}.tradelist li{color:white !important;border-bottom:1px solid #666;}.tbl{background:#3c3c3c !important;}.genelist li:hover,.touchclick:hover{background:#333 !important;}.showbar{background:radial-gradient(at center top,#7B8492,#3c3c3c);}.darklist,.cloud{background-color:#3c3c3c;}.side .hd3,.header,.dropdown ul{background-color:#222;}.list li .sonlist li{background-color:#333;border-color:#555;}.node{background-color:#3b4861;}.rep{background-color:#3b4861;}.btn-gray{background-color:#666;}.btn-white{background-color:#444;color:#999 !important;}.dropmenu .o_btn{margin-right:0;color:#bbb;border-color:#bbb;}.dropmenu .o_btn.select{margin-right:0;color:#fff;border-color:#3498db;background-color:#3498db;}.tipContainer > .list{box-shadow:rgba(0,0,0,0.2) 0px 0px 100px inset !important;}.replyTraceback{background:rgba(0,0,0,0.2) !important;}.sidetitle{background:#222;}form[method="POST"]{color:#999;} `)); const heads = document.getElementsByTagName('head'); if (heads.length > 0) { heads[0].appendChild(node); } else { // no head yet, stick it whereever document.documentElement.appendChild(node); } } else { $('#nightModeStyle').remove(); } }; const setNightMode = (isOn) => { settings.nightMode = isOn; toggleNightMode(); }; switch (settings.autoNightMode.value) { case 'SYSTEM': if (window.matchMedia) { // if the browser/os supports system-level color scheme setNightMode(window.matchMedia('(prefers-color-scheme: dark)').matches); const darkThemeQuery = window.matchMedia('(prefers-color-scheme: dark)'); if (darkThemeQuery.addEventListener) darkThemeQuery.addEventListener('change', (e) => setNightMode(e.matches)); else darkThemeQuery.addListener((e) => setNightMode(e.matches)); // deprecated break; } // eslint-disable-next-line no-fallthrough case 'TIME': { const hour = (new Date()).getHours(); setNightMode(hour > 18 || hour < 7);// TODO: time selector in settings panel break; } default: toggleNightMode(); } /* * 功能:黑条文字鼠标悬浮显示 * param: isOn 是否开启功能 */ const showMarkMessage = (isOn) => { if (isOn) { $(document).on('mouseenter', '.mark', function () { $(this).css({ color: settings.nightMode ? 'rgb(0,0,0)' : 'rgb(255,255,255)' }); }); $(document).on('mouseleave', '.mark', function () { $(this).css({ color: '' }); }); } }; showMarkMessage(settings.hoverUnmark); } onDocumentStart(); function onDOMContentReady() { // run when DOM is loaded let numberOfHttpCSS = 0; let numberOfHttpsCSSLoaded = 0; const httpCSSFixed = () => numberOfHttpsCSSLoaded === numberOfHttpCSS; const fixLinksOnThePage = () => { // 检测纯文本中的链接 const duplicatedSchemeRegex1 = /((href|src)=")((https?:\/\/)+)/g; const duplicatedSchemeRegex2 = /()((https?:\/\/)+)/g; const untaggedUrlRegex = /(?))(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=%]*))(?!("|<\/a>))/g;// https://stackoverflow.com/a/3809435 & https://stackoverflow.com/a/1547940 const fixTextLinksOnThePage = (isOn) => { if (isOn && /(\/(topic|gene|qa|battle|trade)\/\d+)|(\/psnid\/.+?\/comment)|(\/my\/notice)|(\/psngame\/\d+\/comment)|(\/trophy\/\d+)/.test(window.location.href)) $('div.content').each((i, e) => { e.innerHTML = e.innerHTML.replace(duplicatedSchemeRegex1, '$1$4').replace(duplicatedSchemeRegex2, '$1$4').replace(untaggedUrlRegex, '$4'); }); }; // 修复D7VG链接 const linkReplace = (link, substr, newSubstr) => { if (link.href) { link.href = (link.href === link.innerText) ? (link.innerText = link.innerText.replace(substr, newSubstr)) : link.href.replace(substr, newSubstr); } else if (link.src) link.src = link.src.replace(substr, newSubstr); }; const fixD7VGLinksOnThePage = (isOn) => { if (isOn) { $("a[href*='//d7vg.com'], a[href*='//www.d7vg.com']").each((i, a) => { if (!/d7vg\.com($|\/$)/.test(a.href)) { // 排除可能特意指向d7vg.com的链接 linkReplace(a, 'd7vg.com', 'psnine.com'); } }); } }; // 站内使用HTTPS链接 const fixHTTPLinksOnThePage = (isOn) => { if (isOn) { const httpCSS = $("link[href*='http://psnine.com'], link[href*='http://www.psnine.com']"); numberOfHttpCSS = httpCSS.length; httpCSS.each((i, l) => { const replacement = document.createElement('link'); replacement.addEventListener('load', () => { numberOfHttpsCSSLoaded += 1; }, false); replacement.type = 'text/css'; replacement.rel = 'stylesheet'; replacement.href = l.href.replace('http://', 'https://'); l.remove(); document.head.appendChild(replacement); }); $("a[href*='http://psnine.com'], a[href*='http://www.psnine.com'], img[src*='http://psnine.com'], img[src*='http://www.psnine.com'], iframe[src*='http://player.bilibili.com']").each((i, a) => linkReplace(a, 'http://', 'https://')); const scriptSources = []; $("script[src*='http://psnine.com'], script[src*='http://www.psnine.com']").each((i, s) => { scriptSources.push(s.src.replace('http://', 'https://')); s.remove(); }); $('head').find('script').each((i, s) => { if (/^\s*var u\s*=\s*'http:\/\/(www\.)?psnine\.com';\s*$/.test(s.text)) { s.remove(); const replacement = document.createElement('script'); replacement.type = 'text/javascript'; replacement.text = `var u = '${window.location.href.match(/^.+?\.com/)[0]}'`; document.head.appendChild(replacement); return false; } return true; }); const scripts = []; scriptSources.forEach((src) => { $.ajax({ method: 'GET', dataType: 'text', url: src }).then((data) => { const replacement = document.createElement('script'); replacement.type = 'text/javascript'; replacement.text = data; scripts.push({ source: src, script: replacement, }); if (scripts.length === scriptSources.length) { scriptSources.forEach((originalSrc) => { const index = scripts.findIndex((s) => originalSrc.replace('http://', 'https://') === s.source); document.head.appendChild(scripts[index].script); scripts.splice(index, 1); }); } }); }); } }; fixTextLinksOnThePage(settings.fixTextLinks); fixD7VGLinksOnThePage(settings.fixD7VGLinks); fixHTTPLinksOnThePage(settings.fixHTTPLinks); }; fixLinksOnThePage(); Highcharts.setOptions({ lang: { contextButtonTitle: '图表导出菜单', decimalPoint: '.', downloadJPEG: '下载JPEG图片', downloadPDF: '下载PDF文件', downloadPNG: '下载PNG文件', downloadSVG: '下载SVG文件', drillUpText: '返回 {series.name}', loading: '加载中', months: [ '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月', ], noData: '没有数据', numericSymbols: ['千', '兆', 'G', 'T', 'P', 'E'], printChart: '打印图表', resetZoom: '恢复缩放', resetZoomTitle: '恢复图表', shortMonths: [ '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月', ], thousandsSep: ',', weekdays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], }, }); // 暴力猴中已经删掉了GM_addStyle函数,因此需要自己定义 // eslint-disable-next-line camelcase function GM_addStyle(css) { const style = document.getElementById('GM_addStyleBy8626') || (function () { // eslint-disable-next-line no-shadow const style = document.createElement('style'); style.type = 'text/css'; style.id = 'GM_addStyleBy8626'; document.head.appendChild(style); return style; }()); const { sheet } = style; sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length); } // 增加图标 GM_addStyle(` .fa-check-circle { width: 15px; height: 15px; float: left; margin-top: 3px; margin-right: 3px; background: url('data:image/svg+xml;utf8,') no-repeat center; }`); GM_addStyle(` .fa-question-circle { width: 15px; height: 15px; float: left; margin-top: 3px; margin-right: 3px; background: url('data:image/svg+xml;utf8,') no-repeat center; }`); GM_addStyle(` .fa-comments { width: 15px; height: 15px; float: left; margin-top: 3px; margin-right: 3px; background: url('data:image/svg+xml;utf8,') no-repeat center; }`); GM_addStyle(` .fa-coins { width: 15px; height: 15px; float: left; background: url('data:image/svg+xml;utf8,') no-repeat center; }`); // 修复PSPC平台图标显示(临时) const fixPspcIcon = () => { if (/^https?:\/\/psnine\.com\/?$/g.test(window.location.href)) { const $ul = $('div.inner.mt20 > div.side > div.hd3').filter((index, element) => $(element).text().trim() === '游戏评论').next('ul.darklist.pd10'); if ($ul.length > 0) $ul.find('> li > a.l > img').removeAttr('height'); } let pspcIconFixed = true; document.querySelectorAll('span.pf_pspc').forEach((e) => { if (getComputedStyle(e).backgroundColor === 'rgba(0, 0, 0, 0)') pspcIconFixed = false; // 大部分游戏列表 $(e).closest('tr').find('td > a > img.imgbgnb').removeAttr('height'); // psngame页面 $(e).closest('div.box.pd10').children('img.imgbgnb.mr10.l.h-p').removeAttr('height'); // topic页面的关联奖杯列表 $(e).closest('ul.darklist.pd5').find('li > a.l > img').removeAttr('height'); }); if (!pspcIconFixed) { if (/^https?:\/\/psnine\.com\/psngame\/\d+\/?($|\?)/g.test(window.location.href)) $('table.list > tbody > tr > td > img.imgbgnb.l').removeAttr('height'); if (/^https?:\/\/psnine\.com\/qa\/\d+\/?($|\?)/g.test(window.location.href)) $('div.inner.mt40 > div.main > ul.darklist > li > a.l > img').removeAttr('height'); GM_addStyle(` .pf_pspc { font-size: 11px; color: white; border-radius: 2px; padding: 2px 6px; margin-right: 4px; font-weight: 300; background-color: #171d25; }`); // 通用修复:带列表的页面(搜索结果、约战、帖子里的游戏列表)、详情页上的图标修复 GM_addStyle(` .imgbgnb{ object-fit: cover; }`); // 问答详情页中的图标 GM_addStyle(` .darklist img{ object-fit: cover; }`); } }; fixPspcIcon(); // 修复game页面的PS5游戏封面显示(临时) const fixPs5CoverOnGamePages = () => { if (/^https?:\/\/psnine\.com\/game\/\d+/g.test(window.location.href)) { document.querySelectorAll('ul.darklist span.r').forEach((e) => { if (/PS5/g.test(e.innerText)) { const ps5Cover = $(e).closest('li').find('a > img'); if (ps5Cover.length > 0) { // 若为0则表示游戏使用了PS5以外版本的封面,不用进一步修复 ps5Cover.removeAttr('height'); const wrap = $(e).closest('ul.darklist'); const wrapStyle = wrap.attr('style'); const minHeightMatch = wrapStyle.match(/min-height:\s*\d+px/g); if (minHeightMatch) { // 修改min-height值,增加PS5封面高度(91)和PS4封面高度(50)的差值 const minHeightValNew = Number.parseInt(minHeightMatch[0].match(/\d+/g)[0], 10) + (91 - 50); wrap.attr('style', wrapStyle.replace(minHeightMatch[0], `min-height:${minHeightValNew}px`)); } } } }); } }; fixPs5CoverOnGamePages(); /* * 页面右下角追加点击跳转到页面底部按钮 */ const toPageBottom = () => { $('.bottombar').append("B"); $('#scrollbottom').click(() => { $('body,html').animate({ scrollTop: document.body.clientHeight, }, 500); }).css({ cursor: 'pointer', }); }; // 功能0-2:夜间模式 const nightModeStyle = document.getElementById('nightModeStyle'); // ensures that night mode css is after native psnine css if (nightModeStyle) { document.head.appendChild(nightModeStyle); } /* 1.游戏列表添加按难度排列按钮 2.游戏列表根据已记录的完成度添加染色 */ const hdElement = document.querySelector('.hd'); if (hdElement && hdElement.textContent.trim() === '游戏列表') { // 添加徽章 CSS 类 GM_addStyle(` span.completion-badge { background-color: rgb(5 96 175); font-size: 11px; color: white; border-radius: 2px; padding: 2px 6px; margin-right: 4px; font-weight: 300; }`); // 背景 CSS 进度条计算,含夜间模式 const progressPlatinumBG = (p) => `background-image: linear-gradient(90deg, rgba(200,240,255,0.6) ${p}%, rgba(200,255,250,0.15) ${p}%)`; const progressPlatinumBGNight = (p) => `background-image: linear-gradient(90deg, rgba(200,240,255,0.15) ${p}%, rgba(200,255,250,0.05) ${p}%)`; const progressGoldBG = (p) => `background-image: linear-gradient(90deg, rgba(220,255,220,0.8) ${p}%, rgba(220,255,220,0.15) ${p}%);`; const progressGoldBGNight = (p) => `background-image: linear-gradient(90deg, rgba(101,159,19,0.15) ${p}%, rgba(101,159,19,0.05) ${p}%);`; // 获取游戏列表下所有游戏的 DOM 元素指针 const tdElements = document.querySelectorAll('table.list > tbody > tr'); // 获取已保存的完成度 const personalGameCompletions = GM_getValue('personalGameCompletions', []); // 根据已保存的完成度添加染色 tdElements.forEach((tr) => { const gameID = tr.getAttribute('id') || 0; const thisGameCompletion = personalGameCompletions.find((item) => item[0] === gameID); // if game hase platinum 由于个人页面的白金判断是记录的个人完成度,这里需要判断游戏本身是否有白金 const gameHasPlatinum = tr.querySelector('td.pd10 > .meta > em.text-platinum').textContent === '白1'; if (thisGameCompletion) { if (gameHasPlatinum && settings.nightMode) { tr.setAttribute('style', progressPlatinumBGNight(thisGameCompletion[1])); } if (gameHasPlatinum && !settings.nightMode) { tr.setAttribute('style', progressPlatinumBG(thisGameCompletion[1])); } if (!gameHasPlatinum && settings.nightMode) { tr.setAttribute('style', progressGoldBGNight(thisGameCompletion[1])); } if (!gameHasPlatinum && !settings.nightMode) { tr.setAttribute('style', progressGoldBG(thisGameCompletion[1])); } // 添加进度徽章 const gameText = tr.querySelector('td.pd10 > p > a'); if (gameText) { const completion = thisGameCompletion[1]; const completionBadge = document.createElement('span'); completionBadge.className = 'completion-badge'; completionBadge.textContent = `${completion}%`; completionBadge.title = '奖杯完成度'; gameText.parentNode.insertBefore(completionBadge, gameText); } } }); // 添加按难度排列按钮 const spanElement = document.createElement('span'); spanElement.className = 'btn'; spanElement.textContent = '按难度排列'; // 添加 span 元素并设置样式 hdElement.appendChild(spanElement); const style = document.createElement('style'); style.textContent = ` .hd { display: flex; justify-content: space-between; align-items: center; } .hd span { margin-top: 0px; } `; document.head.appendChild(style); // 状态变量,跟踪当前的排序顺序,初始为 false 表示降序 let ascending = false; // 为 span 元素添加点击排序功能 spanElement.addEventListener('click', () => { const tdArray = Array.from(tdElements).map((tr) => { const valueElement = tr.querySelector('td.twoge > em'); const value = valueElement ? parseFloat(valueElement.textContent) : null; return { tr, value }; }); // 根据当前的排序顺序进行排序 tdArray.sort((a, b) => { if (a.value === null) return 1; // a 为空则放到最后 if (b.value === null) return -1; // b 为空则放到最后 return ascending ? a.value - b.value : b.value - a.value; }); const tbody = document.querySelector('table.list tbody'); tbody.innerHTML = ''; tdArray.forEach((item) => { tbody.appendChild(item.tr); }); // 切换排序顺序 ascending = !ascending; }); } /* 用背景进度条显示约战列表中,自己有奖杯记录且未完美的游戏。 */ if (settings.showGameProgressInBattle) { if (/battle$/.test(window.location.href)) { const progressPlatinumBG = (p) => `background-image: linear-gradient(90deg, rgba(200,240,255,0.6) ${p}%, rgba(200,255,250,0.15) ${p}%)`; const progressPlatinumBGNight = (p) => `background-image: linear-gradient(90deg, rgba(200,240,255,0.15) ${p}%, rgba(200,255,250,0.05) ${p}%)`; const personalGameCompletions = GM_getValue('personalGameCompletions', []); const tdElements = document.querySelectorAll('table.list > tbody > tr'); tdElements.forEach((tr) => { const gameID = tr.querySelector('td.pdd15 a').href.match(/\/psngame\/(\d+)/)[1]; const thisGameCompletion = personalGameCompletions.find((item) => item[0] === gameID); if (thisGameCompletion && thisGameCompletion[1] < 100) { // 约战页面没有显示游戏本身是否有白金,就直接默认白金底色显示了 if (settings.nightMode) { tr.setAttribute('style', progressPlatinumBGNight(thisGameCompletion[1])); } if (!settings.nightMode) { tr.setAttribute('style', progressPlatinumBG(thisGameCompletion[1])); } } }); } } /* ↓↓↓ 约战监控与通知相关功能开始 ↓↓↓↓ 1. 用户是否设置了监控 2. 约战缓存是否存在或过期,否则从约战页更新数据 3. 比较两组数据,并更新顶部菜单 */ // 添加消息通知数量图标样式(伪元素) GM_addStyle(` .notice::after { content: attr(data-notice); position: absolute; top: 8px; right: 0px; background-color: red; color: white; border-radius: 6px; padding: 2px 2px; font-size: 12px; line-height: 0.9em; display: inline-block; min-width: 12px; text-align: center; }`); // 定义两个变量,用户设置的游戏约战监控列表,和当前存在的约战列表(缓存) let userBattleMonitors = GM_getValue('userBattleMonitors', []); let cacheBattleInfo = GM_getValue('cacheBattleInfo', {}); const updateTopMenuNotice = (lista, listb) => { // 定义函数:变更顶部菜单通知红点,多处执行 let count = 0; lista.forEach((item) => { if (listb.includes(item)) count += 1; }); if (count > 0) { document.querySelectorAll('#pcmenu li, #mobilemenu li').forEach((el) => { const a = el.querySelector('a'); if (a && a.innerText === '约战') { el.classList.add('notice'); el.setAttribute('data-notice', count); } }); } else { document.querySelectorAll('#pcmenu li, #mobilemenu li').forEach((el) => el.classList.remove('notice')); } }; const updateBattleRecuritInfo = () => { // 定义函数:更新约战信息 const result = []; $.ajax({ type: 'GET', url: 'https://psnine.com/battle', dataType: 'html', async: true, success(data, status) { if (status === 'success') { const page = document.createElement('html'); page.innerHTML = data; const list = page.querySelectorAll('.box table.list > tbody > tr'); list.forEach((tr) => { const gameID = tr.querySelector('td.pdd15 a').href.match(/\/psngame\/(\d+)/)[1]; result.push(gameID); }); cacheBattleInfo = { list: result, lastUpdate: new Date().getTime() }; GM_setValue('cacheBattleInfo', cacheBattleInfo); updateTopMenuNotice(userBattleMonitors, cacheBattleInfo.list); } }, error: () => { console.log('无法获取约战信息'); }, }); }; // 页面加载时执行约战监测功能 if (cacheBattleInfo.lastUpdate && new Date().getTime() - cacheBattleInfo.lastUpdate < settings.BattleInfoUpdateInterval) { updateTopMenuNotice(userBattleMonitors, cacheBattleInfo.list); } else { updateBattleRecuritInfo(); } // 在游戏的约战页面添加约战监控按钮 if (/\/psngame\/\d+\/battle\/?$/.test(window.location.href)) { const gameID = window.location.href.match(/\/psngame\/(\d+)/)[1]; const actionArea = document.querySelector('center.pd10'); const monitorBTN = document.createElement('p'); monitorBTN.className = 'btn btn-large btn-info'; monitorBTN.title = '当有用户发起该游戏的约战时,顶部菜单会出现红点通知。'; monitorBTN.textContent = userBattleMonitors.includes(gameID) ? '移除约战监控' : '添加约战监控'; // 添加 span 元素并设置样式 actionArea.appendChild(monitorBTN); const style = document.createElement('style'); style.textContent = ` center.pd10 { display: flex; justify-content: space-between; align-items: center; } center.pd10 * { flex: 1; width: calc(50% - 8px); margin: 0 4px; }`; document.head.appendChild(style); // 为 span 元素添加点击事件,切换约战监控状态 monitorBTN.addEventListener('click', () => { if (userBattleMonitors.includes(gameID)) { userBattleMonitors = userBattleMonitors.filter((id) => id !== gameID); monitorBTN.textContent = '添加约战监控'; } else { userBattleMonitors.push(gameID); monitorBTN.textContent = '移除约战监控'; } GM_setValue('userBattleMonitors', userBattleMonitors); updateTopMenuNotice(userBattleMonitors, cacheBattleInfo.list); }); } /* ↑↑↑↑ 约战监控与通知相关功能结束 ↑↑↑↑ */ /* * 自动签到功能 * @param isOn 是否开启功能 */ const repeatUntilSuccessful = (functionPtr, interval) => { if (!functionPtr()) { setTimeout(() => { repeatUntilSuccessful(functionPtr, interval); }, interval); } }; const automaticSignIn = (isOn) => { // 如果签到按钮存在页面上 if (isOn && $('[class$=yuan]').length > 0) { repeatUntilSuccessful(() => { if (typeof qidao !== 'function') return false; let signed = false; $('[class$=yuan]').each((i, e) => { if (!signed && /^\s*签\s*$/.test(e.innerText)) { e.click(); signed = true; } }); return true; }, 200); } }; automaticSignIn(settings.autoCheckIn); /* * 获取当前页面的后一页页码和链接 * @return nextPage 后一页页码 * @return nextPageLink 后一页的链接 */ const getNextPageInfo = () => { // 获取下一页页码 const nextPage = Number($('.page > ul > .current:last').text()) + 1; // 如果地址已经有地址信息 let nextPageLink = ''; if (/page/.test(window.location.href)) { nextPageLink = window.location.href.replace( /page=.+/, `page=${nextPage}`, ); } else { nextPageLink = `${window.location.href}&page=${nextPage}`; } return { nextPage, nextPageLink }; }; GM_addStyle( `#loadingMessage { position : absolute; bottom : 0px; position : fixed; right : 1px !important; display : none; color : white; }`, ); /* 在 LocatStorage 中保存个人游戏完成度函数,为避免过多的页面重复请求,逻辑梳理如下: 场景: 1. 更新的奖杯一定在前,但用户可能会隐藏游戏或解除隐藏,导致内容不再确定。 2. 隐藏游戏条目不影响本地已有数据,也不要求本地数据作对应删除,但影响页码数和对应的更新记录 3. 由于设置为 Ajax 5 秒翻页,所以存在前几页数据已更新,后几页数据因为用户关闭页面而未更新的情况 4. 用户新开坑,可能导致页面数量增长,此时,最后几页也未记录更新时间,但实际是不需要更新的 简化: 1. 没有时间记录的页面,都需要更新,有时间记录的页面,从前往后更新,遇到无变化奖杯条目的就停止 2. 当用户进行异常操作时,需要自行通过翻页刷新数据 */ // 测试用清除数据 // GM_setValue('personalGameCompletions', []); // console.log(GM_getValue('personalGameCompletions', [])); // GM_setValue('personalGameCompletionsLastUpdate', []); const parseCompletionPage = (content) => { // 游戏进度信息 const tdElements = content.querySelectorAll('table.list tbody > tr'); const thisPageCompletions = Array.from(tdElements).map((tr) => { const completionElement = tr.querySelector('div.progress > div'); const completion = completionElement ? parseFloat(completionElement.textContent) : 0; const platinumElement = tr.querySelector('span.text-platinum'); const platinum = platinumElement ? platinumElement.textContent === '白1' : false; const gameIDElement = tr.querySelector('a'); const gameID = gameIDElement.href.match(/\/psngame\/(\d+)/)[1]; return [gameID, completion, platinum]; }); // 获得总页数和总条目数 let totalPages = 0; let totalItems = 0; const PaginationEle = content.querySelectorAll('.page > ul > li > a'); if (PaginationEle.length > 2) { totalPages = parseInt(PaginationEle[PaginationEle.length - 2].innerText, 10); totalItems = parseInt(PaginationEle[PaginationEle.length - 1].innerText, 10); } return { totalPages, totalItems, thisPageCompletions }; }; const updateCompletions = (updateList) => { const gameCompletionHistory = GM_getValue('personalGameCompletions', []); let addCounts = 0; updateList.forEach((completion) => { const index = gameCompletionHistory.findIndex((historyItem) => historyItem[0] === completion[0]); if (index !== -1) { if (gameCompletionHistory[index][1] !== completion[1]) { addCounts += 1; } gameCompletionHistory[index] = completion; } else { gameCompletionHistory.push(completion); addCounts += 1; } }); GM_setValue('personalGameCompletions', gameCompletionHistory); const totalRecords = gameCompletionHistory.length; return { addCounts, totalRecords }; }; // 后台更新主函数 const loadGameCompletions = (userid, startPageID) => { // console.log(`https://psnine.com/psnid/${userid}/psngame?page=${startPageID}`) $.ajax({ type: 'GET', url: `https://psnine.com/psnid/${userid}/psngame?page=${startPageID}`, dataType: 'html', async: true, success: (data, status) => { if (status === 'success') { // 读取历史数据 let pagesUpdateTime = GM_getValue('personalGameCompletionsLastUpdate', []); // 2024.07.30 bug fix: 错误地保存他人的游戏完成度 - 已经修复,但用户端的旧数据需要清除 if (Array.isArray(pagesUpdateTime) === false || pagesUpdateTime[0] === undefined || pagesUpdateTime[0] < 1722333600000 // 2024-07-30 18:00 GMT+0800 ) { GM_setValue('personalGameCompletions', []); pagesUpdateTime = []; } // 读取当前页奖杯完成数据 const page = document.createElement('html'); page.innerHTML = data; const o = parseCompletionPage(page); const { thisPageCompletions } = o; const totalPages = o.totalPages || pagesUpdateTime.length || 1; const { addCounts } = updateCompletions(thisPageCompletions); // 更新页面记录时间 pagesUpdateTime[startPageID - 1] = new Date().getTime(); GM_setValue('personalGameCompletionsLastUpdate', pagesUpdateTime); // 根据规则计算下一页 if (addCounts === thisPageCompletions.length && startPageID < totalPages - 1) { setTimeout(() => { loadGameCompletions(userid, startPageID + 1); }, 5000); return true; } const fullfilledUpdateTime = pagesUpdateTime.concat(Array(totalPages - pagesUpdateTime.length).fill(0)); const nextIdx = fullfilledUpdateTime.findIndex((time) => time === undefined || time === 0 || time === null); if (nextIdx !== -1) { setTimeout(() => { loadGameCompletions(userid, nextIdx + 1); }, 5000); return true; } return false; } return true; }, error: (e) => { console.log('loadGameCompletions error', e); }, }); }; // 获取个人 ID const myHomePage = document.querySelectorAll('ul.r li.dropdown ul li a')[0].href; const myUserId = myHomePage.match(/\/psnid\/([A-Za-z0-9_-]+)/)[1] || '*'; // const myGamePageURLRegex = new RegExp(`psnid/${myUserId}/?(?:psngame(?:\\?page=(\\d+))?|$)`); const myHomepageURLRegex = new RegExp(`psnid/${myUserId}/?`); const myGamePageURLRegex = new RegExp(`psnid/${myUserId}/psngame(?:\\?page=(\\d+))?`); // 后台更新频次控制 const bgUpdateMyGameCompletion = () => { const pagesUpdateTime = GM_getValue('personalGameCompletionsLastUpdate', []); const now = new Date().getTime(); if (pagesUpdateTime[0] === undefined || now - pagesUpdateTime[0] > 60 * 60 * 1000) { loadGameCompletions(myUserId, 1); } }; // 在用户浏览个人页面或个人游戏列表页时,无视 Interval 白嫖一次数据更新 if (myGamePageURLRegex.test(window.location.href)) { const pageid = parseInt(window.location.href.match(myGamePageURLRegex)[1], 10) || 1; const { totalItems, thisPageCompletions } = parseCompletionPage(document); const { totalRecords } = updateCompletions(thisPageCompletions); const pagesUpdateTime = GM_getValue('personalGameCompletionsLastUpdate', []); pagesUpdateTime[pageid - 1] = new Date().getTime(); GM_setValue('personalGameCompletionsLastUpdate', pagesUpdateTime); if (totalRecords < totalItems || totalItems === 0) { const nextPageID = pageid === 1 ? 2 : 1; loadGameCompletions(myUserId, nextPageID); } } else if (myHomepageURLRegex.test(window.location.href)) { const { thisPageCompletions } = parseCompletionPage(document); updateCompletions(thisPageCompletions); const pagesUpdateTime = GM_getValue('personalGameCompletionsLastUpdate', []); pagesUpdateTime[0] = new Date().getTime(); GM_setValue('personalGameCompletionsLastUpdate', pagesUpdateTime); } else { bgUpdateMyGameCompletion(); // 定时更新 } /// ///////////////////////////////////////////////////////////////////////////////// /* 在奖杯页提供扩展功能,把每个奖杯页的评论直接展示在当前页面。 可以单点展开一个奖杯的 tips。 // 一次性展开所有奖杯 tips 的逻辑可能会造成服务器压力过大,已隐藏 同时改进页面默认的排序功能并阻止页面跳转行为。 */ // 节流,防止用户多次点击 const throttleDebounce = (func, delay) => { let timeout = null; return (...args) => { if (!timeout) { func.apply(this, args); timeout = setTimeout(() => { timeout = null; }, delay); } }; }; // const myGameTrophyPageRegex = new RegExp(`psngame/(\\d+)\\?psnid=${myUserId}`); // if (myGameTrophyPageRegex.test(window.location.href)) { // 不再限制到自己的游戏页,现在在别人的游戏页也会执行 const gameTrophyPageRegex = new RegExp('psngame/\\d+\\?psnid='); if (gameTrophyPageRegex.test(window.location.href)) { const height = Math.min(Math.max(window.innerHeight - 100, 320), 1200); GM_addStyle(`.tipContainer > .list {max-height:${height}px; overflow-y:auto; box-shadow:inset 0 0 100px rgba(0,0,0,0.05); padding: 10px 0; border-left: 2px solid #00a8e6;}`); GM_addStyle('.tipContainer { padding: 10px 10px 10px 84px; margin: 0;}'); GM_addStyle('.tipContainer > ul.list > li {padding: 4px 14px 4px 8px;}'); GM_addStyle('.tipContainer > ul.list > li:first-child { padding:4px 14px 4px 8px;}'); GM_addStyle('table.list td > p > em.alert-success{cursor:pointer}'); GM_addStyle('table.list td > p > em.alert-success::after{content:""; width:0; height:0px; border-top:5px solid #659f13; border-left: 5px solid transparent; border-right: 5px solid transparent; margin-left: 7px; display: inline-block; position: relative; top: -2px}'); const trophyTables = Array.from(document.querySelectorAll('table.list')); // every dlc has one table const thisPageTrophyList = trophyTables .flatMap((table) => Array.from(table.querySelectorAll('tr[id]')) .map((tr) => { const ID = parseInt(tr.id, 10); const tds = Array.from(tr.querySelectorAll('td')); const trophyLink = tds[0].querySelector('a').href; const trophyTypeMatch = tds[0].className.match(/\b(t1|t2|t3|t4)\b/); const trophyType = trophyTypeMatch ? trophyTypeMatch[1] : ''; const tipNumberEle = tds[1].querySelector('p em.alert-success b'); const tipNumber = tipNumberEle ? parseInt(tipNumberEle.innerText, 10) : 0; const earned = tds.length === 4 && !!tds[2].querySelector('em'); const percentage = parseFloat(tds[tds.length - 1].innerText) || 0; return { ID, trophyLink, trophyType, tipNumber, earned, percentage, trDom: tr, table, tipListDom: null, tipShow: false, }; })); // 添加对象代理以便数据更新后自动渲染对应 DOM,并且在 tipShow 为 true 时自动加载 const myTrophyList = thisPageTrophyList.map((item) => new Proxy(item, { set: (target, prop, value) => { let flag = false; if (prop === 'tipListDom' || prop === 'tipShow') { flag = true; } target[prop] = value; // eslint-disable-next-line no-use-before-define if (flag) { refreshTrophyTip(); } return true; }, })); // 根据当前状态刷新 tipListDom,维护列表的正常展示顺序 const refreshTrophyTip = () => { // eslint-disable-next-line no-use-before-define mutationsOff(); myTrophyList.filter((t) => t.tipListDom).forEach((t) => { if (t.trDom.style.display !== 'none' && t.tipShow === true) { // 应当显示 t.trDom.insertAdjacentElement('afterend', t.tipListDom); // 插入或移动 } else { t.tipListDom.remove(); // 不显示则移出文档,重复 remove() 无影响 } }); // eslint-disable-next-line no-use-before-define mutationsOn(); }; // 独立实现黑名单与屏蔽词,因为只在 getTipContent() 中用到一次。 // 旧版的黑名单与屏蔽词函数是基于页面当前 dom 渲染的,会在 refreshTrophyTip 后重置 const userListLowerCase = settings.blockList.map((user) => user.toLowerCase()); const blockWordsList = settings.blockWordsList.map((word) => word.toLowerCase()); const filterBlockUser = (rootEle, itemSelector, itemAuthorSelector) => { const items = rootEle.querySelectorAll(itemSelector); if (items.length > 0) { items.forEach((item) => { const authorEle = item.querySelector(itemAuthorSelector); if (authorEle) { const author = authorEle.innerText.toLowerCase(); if (userListLowerCase.includes(author.toLowerCase())) { item.remove(); } } }); } }; const filterBlockWords = (rootEle, itemSelector, contentSelector) => { const posts = rootEle.querySelectorAll(itemSelector); if (posts.length > 0) { posts.forEach((post) => { const contentEle = post.querySelector(contentSelector); if (contentEle) { const content = contentEle.textContent.toLowerCase(); if (blockWordsList.some((word) => content.includes(word))) { const warningDiv = document.createElement('div'); warningDiv.textContent = '====== 内容包含您的屏蔽词,点击查看屏蔽内容 ======'; warningDiv.className = 'btn btn-gray font12'; warningDiv.style.marginBottom = '2px'; warningDiv.onclick = () => { warningDiv.previousElementSibling.style.display = 'block'; warningDiv.style.display = 'none'; }; post.style.display = 'none'; post.insertAdjacentElement('afterend', warningDiv); } } }); } }; // AJAX 获取奖杯评论并添加数据到对象代理中,由对象代理的 set 函数自行触发更新 const getTipContent = (t) => { $.ajax({ type: 'GET', url: `${t.trophyLink}`, dataType: 'html', async: true, success: (data, status) => { if (status === 'success') { // get content from page const page = document.createElement('html'); page.innerHTML = data; // 两种不同的列表页面样式,一种是ul.list 列表,一种是 div.post DOM 组 const comments = page.querySelector('ul.list'); const posts = page.querySelectorAll('div.post'); // wrap for table const tipTR = document.createElement('tr'); const tipTD = document.createElement('td'); tipTD.colSpan = 4; tipTD.classList.add('tipContainer'); tipTR.appendChild(tipTD); if (comments) { // blacklist and block words filterBlockUser(comments, 'ul.list>li', 'div.ml64>.meta.pb10>.psnnode'); filterBlockUser(comments, 'ul.sonlist>li', '.content>.psnnode'); filterBlockWords(comments, 'ul.list>li', 'div.ml64>div.content.pb10'); tipTD.appendChild(comments); } else if (posts) { // 包裹 .list 以便适用于同一套 CSS 样式 const listdiv = document.createElement('div'); posts.forEach((post) => { listdiv.appendChild(post); }); listdiv.classList.add('list'); // blacklist and block words filterBlockUser(listdiv, '.list>.post', '.meta>.psnnode'); filterBlockWords(listdiv, '.list>.post', 'div.ml64>div.content.pb10'); tipTD.appendChild(listdiv); } t.tipListDom = tipTR; } return true; }, error: (e) => { console.log('getTipContent error', e); }, }); }; // 为 trophy tip badges 添加 click 事件,开关切换 tipShow myTrophyList.forEach((t) => { const mainColumn = t.trDom.querySelectorAll('td')[1]; const trophyTipEle = mainColumn.querySelector('p em.alert-success'); if (trophyTipEle) { const throttleGetTipContent = throttleDebounce(() => { getTipContent(t); t.tipShow = true; }, 2000); trophyTipEle.addEventListener('click', (event) => { if (!t.tipListDom) { throttleGetTipContent(event); } else { t.tipShow = !t.tipShow; // 当状态变化时会触发 set 函数 } }); } }); // 定义 trophyTables 的 mutation on off 函数 const observers = []; const mutationsOff = () => { observers.forEach((worker) => worker.observer.disconnect()); }; const mutationsOn = () => { observers.forEach((worker) => worker.observer.observe(worker.target, worker.config)); }; const handleMutation = (mutations) => { let refreshFlag = false; mutations.forEach((mutation) => { if (mutation.target.matches('tr.trophy') ) { refreshFlag = true; } }); if (refreshFlag) { refreshTrophyTip(); } }; trophyTables.forEach((table) => { const observer = new MutationObserver(handleMutation); const target = table.querySelector('tbody'); const config = { attributes: true, childList: true, subtree: true }; observers.push({ observer, target, config }); mutationsOn(); }); // 一次性展开不能直接开放给所有用户,可能造成服务器负担 const konamiCode = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight']; let currentStep = 0; window.addEventListener('keydown', (e) => { if (e.key === konamiCode[currentStep]) { currentStep += 1; if (currentStep === konamiCode.length) { // 添加 『展开全部未完成奖杯 Tips』文字按钮 GM_addStyle('table.list tr:first-child td {position: relative;}'); GM_addStyle('table.list tr .ml100 p#expand { font-size: 12px; position: absolute; right: 12%; bottom: 0; padding: 0;margin: 0;}'); GM_addStyle('table.list tr .ml100 p#expand a { cursor: pointer; text-decoration: none; color: #999; margin: 0 4px; }'); // add text button const expandBtnContainer = document.createElement('p'); expandBtnContainer.id = 'expand'; const expandUndoneBtn = document.createElement('a'); expandUndoneBtn.innerText = '展开未完成奖杯 Tips'; const expandAllBtn = document.createElement('a'); expandAllBtn.innerText = '展开所有奖杯 Tips'; expandBtnContainer.appendChild(expandUndoneBtn); expandBtnContainer.appendChild(expandAllBtn); trophyTables[0].querySelector('tr td div').appendChild(expandBtnContainer); // click 事件的 multipleTipLoading 函数 let multipleTipLoadingFlag = false; let openAllTipFlag = true; let openUndoneTipFlag = true; const multipleTipLoading = (type) => { if (type === 'undone') { myTrophyList.filter((t) => !t.earned).forEach((t) => { t.tipShow = openUndoneTipFlag; }); openUndoneTipFlag = !openUndoneTipFlag; expandUndoneBtn.innerText = '加载中 ...'; expandAllBtn.innerText = '等待中 ...'; } else { myTrophyList.forEach((t) => { t.tipShow = openAllTipFlag; }); openAllTipFlag = !openAllTipFlag; expandAllBtn.innerText = '加载中 ...'; expandUndoneBtn.innerText = '等待中 ...'; } multipleTipLoadingFlag = true; let tasklist = myTrophyList.filter((t) => !t.tipListDom && t.tipNumber > 0); if (type === 'undone') { tasklist = tasklist.filter((t) => !t.earned); } function recursiveLoad() { if (tasklist.length > 0) { const t = tasklist.shift(); t.tipShow = true; getTipContent(t); setTimeout(() => { recursiveLoad(); }, 1500); } else { multipleTipLoadingFlag = false; expandUndoneBtn.innerText = openUndoneTipFlag ? '展开未完成奖杯 Tips' : '收起未完成奖杯 Tips'; expandAllBtn.innerText = openAllTipFlag ? '展开所有奖杯 Tips' : '收起所有奖杯 Tips'; return true; } return false; } recursiveLoad(); }; // 为 expandAllBtn 添加 click 事件 expandAllBtn.addEventListener('click', (event) => { event.stopImmediatePropagation(); event.preventDefault(); if (multipleTipLoadingFlag === true) return; multipleTipLoading(); }); // 为 expandUndoneBtn 添加 click 事件 expandUndoneBtn.addEventListener('click', (event) => { event.stopImmediatePropagation(); event.preventDefault(); if (multipleTipLoadingFlag === true) return; multipleTipLoading('undone'); }); } } else { currentStep = 0; } }); // 取消奖杯排序菜单的页面跳转,并重新实现排序 const sortFlag = { XMB: true, trophyType: true, percentage: true }; const sortByType = (type) => { if (type === 'XMB') { myTrophyList.sort((a, b) => (sortFlag.XMB ? a.ID - b.ID : b.ID - a.ID)); } if (type === 'trophyType') { myTrophyList.sort((a, b) => a.trophyType.localeCompare(b.trophyType) * (sortFlag.trophyType ? 1 : -1)); } if (type === 'percentage') { myTrophyList.sort((a, b) => (a.percentage - b.percentage) * (sortFlag.percentage ? 1 : -1)); } sortFlag[type] = !sortFlag[type]; myTrophyList.forEach((item) => { item.trDom.remove(); item.table.appendChild(item.trDom); }); }; const sortMenuItemsEle = document.querySelectorAll('div.main div.box ul.dropmenu > li.dropdown > ul >li'); sortMenuItemsEle[0].addEventListener('click', (event) => { event.stopImmediatePropagation(); event.preventDefault(); sortByType('XMB'); }); sortMenuItemsEle[1].addEventListener('click', (event) => { event.stopImmediatePropagation(); event.preventDefault(); sortByType('trophyType'); }); sortMenuItemsEle[2].addEventListener('click', (event) => { event.stopImmediatePropagation(); event.preventDefault(); sortByType('percentage'); }); } /// ////////////////////////////////////////////////////////////////////////////// if ( /psnid\/[A-Za-z0-9_-]+\/?$/.test(window.location.href) && $('tbody').length > 2 ) { const windowLocationHref = window.location.href.replace(/\/$/g, ''); // 功能0-7:个人主页下显示所有游戏 if (settings.autoPagingInHomepage) { let isbool2 = true; // 触发开关,防止多次调用事件 // 插入加载提示信息 $('body').append("
"); let gamePageIndex = 2; $(window).scroll(function () { if ( $(this).scrollTop() + $(window).height() + 700 >= $(document).height() && $(this).scrollTop() > 700 && isbool2 === true ) { isbool2 = false; const gamePage = `${windowLocationHref}/psngame?page=${gamePageIndex}`; // 加载页面并且插入 $('#loadingMessage').text(`加载第${gamePageIndex}页...`).show(); $.get( gamePage, {}, (data) => { const $response = $('
').html(data); const nextGameContent = $response.find('tbody > tr'); if (nextGameContent.length > 0) { $('tbody > tr:last').after(nextGameContent); isbool2 = true; gamePageIndex += 1; fixPspcIcon(); } else { $('#loadingMessage').text('没有更多游戏了...'); } }, 'html', ); setTimeout(() => { $('#loadingMessage').fadeOut(); }, 2000); } }); } // 功能:未注册用户的PSN主页添加更新按钮 const updateButtonForm = $('div.psnzz > div.inner > div.psnbtn.psnbtnright > form'); if (updateButtonForm.find('a').length === 0) { const upbase = `等级同步`; const upgame = `游戏同步`; updateButtonForm.append(upbase, upgame); } } // 帖子优化 /* * 功能:对发帖楼主增加“楼主”标志 * @param userId 用户(楼主)ID */ const addOPBadge = (userId) => { $('.psnnode').each((i, n) => { // 匹配楼主ID,变更CSS if ($(n).text() === userId) { $(n).after('楼主'); } }); }; /* * AJAX获取页面 */ const fetchOtherPage = (url, successFunction) => { let resultSet; $.ajax({ type: 'GET', url, dataType: 'html', async: true, success(data, status) { if (status === 'success') { resultSet = successFunction(data); $('.imgbgnb').parent().each((index, el) => { resultSet.forEach((element) => { if (element.trophy === $(el).attr('href')) { $(el).next().find('a').slice(0, 1) .append(`
 ${element.earned}`); } }); }); } }, error: () => { console.log('无法获取页面信息'); }, }); }; const getEarnedTrophiesInfo = (data) => { const reg = /[\s\S]*<\/body>/g; const html = reg.exec(data)[0]; const resultSet = []; $(html).find('tbody>tr[id]').find('.imgbg.earned').parent() .parent() .parent() .each((index, el) => { const earnedTime = $(el).find('em.lh180.alert-success.pd5.r'); const earnedTimeCopy = earnedTime.clone(); earnedTimeCopy.find('br').replaceWith(' '); resultSet.push({ trophy: $(el).find('a').attr('href'), earned: `${earnedTime.attr('tips').trim()} ${earnedTimeCopy.text().trim()}`, }); }); return resultSet; }; if (/topic\//.test(window.location.href) && psnidCookie) { const games = {}; $('.imgbgnb').parent().each((index, el) => { if (!/(^| |")(pd10|t3)($| |")/.test($(el).parent().get()[0].className)) return; const href = $(el).attr('href'); const gameId = href.slice(href.lastIndexOf('/') + 1, -3); // 根据具体游戏获取对应自己页面的信息 if (!Object.prototype.hasOwnProperty.call(games, gameId)) { const gamePageUrl = `${document.URL.match(/^.+?\.com/)[0]}/psngame/${gameId}?psnid=${psnidCookie[1]}`; fetchOtherPage(gamePageUrl, getEarnedTrophiesInfo); games[gameId] = true; } }); } if ( /(gene|trade|topic)\//.test(window.location.href) && !/comment/.test(window.location.href) ) { // 获取楼主ID const authorId = $('.title2').text(); addOPBadge(authorId); } /* * 功能:对关注用户进行ID高亮功能函数 */ const addHighlightOnID = () => { settings.highlightSpecificID.forEach((i) => { $(`.meta>[href="${window.location.href.match('(.*)\\.com')[0]}/psnid/${i}"]`).css({ 'background-color': settings.highlightSpecificBack, color: settings.highlightSpecificFront, }); }); }; addHighlightOnID(); /* * 功能:根据纯文本的长度截断DOM * @param elem 需要截断的DOM * @param length 需要保留的纯文本长度 * @return 截断后的 html 文本 */ const truncateHtml = (elem, length) => { // 递归获取 DOM 里的纯文本 const truncateElem = (e, reqCount) => { let grabText = ''; let missCount = reqCount; $(e).contents().each(function () { switch (this.nodeType) { case Node.TEXT_NODE: { // Get node text, limited to missCount. grabText += this.data.substr(0, missCount); missCount -= Math.min(this.data.length, missCount); break; } case Node.ELEMENT_NODE: { // Explore current child: const childPart = truncateElem(this, missCount); grabText += childPart.text; missCount -= childPart.count; break; } default: { break; } } if (missCount === 0) { // We got text enough, stop looping. return false; } return true; }); return { // Wrap text using current elem tag. text: `${e.outerHTML.match(/^<[^>]+>/m)[0] + grabText}`, count: reqCount - missCount, }; }; return truncateElem(elem, length).text; }; /* * 功能:回复内容回溯,仅支持机因、主题 * @param isOn 是否开启功能 */ const showReplyContent = (isOn) => { if (isOn) { // 每一层楼的回复框 const allSource = $('.post .ml64 > .content'); if (allSource.length <= 0) return; GM_addStyle( `.replyTraceback { background-color: rgb(0, 0, 0, 0.04); padding: 10px !important; color: rgb(160, 160, 160, 1) !important; }`, ); // 悬浮框内容左对齐样式 GM_addStyle(` .tippy-content { text-align: left; overflow-wrap: break-word; }`); // 每一层楼的回复者用户名 const userId = $('.post > .ml64 > [class$=meta]'); // 每一层的头像 const avator = $('.post > a.l'); for (let floor = allSource.length - 1; floor > 0; floor -= 1) { // 层内内容里包含链接(B的发言中是否有A) const content = allSource.eq(floor).find('a'); if (content.length > 0) { for (let userNum = 0; userNum < content.length; userNum += 1) { // 对每一个链接的文本内容判断 const linkContent = content.eq(userNum).text().match('@(.+)'); // 链接里是@用户生成的链接, linkContent为用户名(B的发言中有A) if (linkContent !== null) { // 从本层的上一层开始,回溯所@的用户的最近回复(找最近的一条A的发言) let traceIdFirst = -1; let traceIdTrue = -1; for (let traceId = floor - 1; traceId >= 0; traceId -= 1) { // 如果回溯到了的话,选取内容 // 回溯层用户名 const thisUserID = userId.eq(traceId).find('.psnnode:eq(0)').text(); if (thisUserID.toLowerCase() === linkContent[1].toLowerCase()) { // 判断回溯中的@(A的发言中有是否有B) if ( allSource.eq(traceId).text() === userId.eq(floor).find('.psnnode:eq(0)').text() ) { traceIdTrue = traceId; break; } else if (traceIdFirst === -1) { traceIdFirst = traceId; } } } let outputID = -1; if (traceIdTrue !== -1) { outputID = traceIdTrue; } else if (traceIdFirst !== -1) { outputID = traceIdFirst; } // 输出 if (outputID !== -1) { const replyContentObjectOriginal = allSource.eq(outputID); const replyContentObject = replyContentObjectOriginal.clone(); const replyContentPlainText = replyContentObject.text(); replyContentObject.find('.mark').text((index, text) => `${text}`); const replyContentText = replyContentObject.text(); let replyContentTruncatedText = $(truncateHtml($('

').html(replyContentText)[0], 45)).html(); if (replyContentPlainText.length > 45) { replyContentTruncatedText += '......'; } const avatorImg = avator.eq(outputID).find('img:eq(0)').attr('src'); allSource.eq(floor).before( `
${linkContent[1]} ${replyContentTruncatedText}
`, ); // 如果内容超过45个字符,则增加悬浮显示全文内容功能 if (replyContentPlainText.length > 45) { tippy(`.responserContent_${floor}_${outputID}`, { content: replyContentText, animateFill: false, maxWidth: '500px', }); } // 增加点击回复内容跳转功能 const responserContent = $(`.responserContent_${floor}_${outputID}`); responserContent.click(() => { const targetBounds = replyContentObjectOriginal.get(0).getBoundingClientRect(); const currentBoundsTop = responserContent.get(0).getBoundingClientRect().top; if (targetBounds.top < 0) { // 回复内容顶部不在窗口内 // 回复内容顶部滚动到当前元素顶部处无法完整显示时 if (currentBoundsTop + targetBounds.height > window.innerHeight) { // 回复内容比窗口高度更长时,回复内容顶部滚动至窗口顶部,否则回复内容底部滚动至窗口底部 if (targetBounds.height > window.innerHeight) window.scrollBy({ top: targetBounds.top, behavior: 'smooth' }); else window.scrollBy({ top: targetBounds.bottom - window.innerHeight, behavior: 'smooth' }); } else if (currentBoundsTop < 0) window.scrollBy({ top: targetBounds.top, behavior: 'smooth' }); // 当前元素顶部在窗口外时 else window.scrollBy({ top: targetBounds.top - currentBoundsTop, behavior: 'smooth' }); // 默认滚动至当前元素顶部 } $(replyContentObjectOriginal) .fadeOut(500) .fadeIn(500) .fadeOut(500) .fadeIn(500); }); // 鼠标悬浮变手形样式 responserContent.css('cursor', 'pointer'); } } } } } } }; /* * 功能:增加帖子楼层信息 */ const addFloorIndex = () => { let baseFloorIndex = 0; let subFloorIndex = -1; $('span[class^=r]').each((i, el) => { if (i > 0) { if ($(el).attr('class') === 'r') { $(el).children('a:last') .after(`  #${baseFloorIndex}`); baseFloorIndex += 1; subFloorIndex = -1; } else { $(el).children('a:last') .after( `  #${baseFloorIndex}${subFloorIndex}`, ); subFloorIndex -= 1; } } }); }; /* * 功能:热门帖子增加 热门 标签 */ const addHotTag = () => { $('div.meta').each((index, element) => { const replyCount = $(element).text().split(/(\d+)/); if (Number(replyCount[replyCount.length - 2]) > settings.hotTagThreshold && replyCount[replyCount.length - 1].match('评论|答案|回复') && replyCount[replyCount.length - 1].match('评论|答案|回复').index > -1 && $(element).children('a#hot').length === 0 ) { const tagBackgroundColor = $('body.bg').css('background-color'); $(element) .append(` 🔥热门 `); } }); }; addHotTag(); /* * 功能:层内逆序显示 * @param isOn 是否开启该功能 */ const reverseSubReply = (isOn) => { if (!isOn || !/(\/trophy\/\d+)|(\/psngame\/\d+\/comment)|(\/psnid\/.+?\/comment)/.test(window.location.href)) return; repeatUntilSuccessful(() => { try { $('div.btn.btn-white.font12').click(); const blocks = $('div.sonlistmark.ml64.mt10:not([style="display:none;"])'); blocks.each((index, block) => { const reversedBlock = $($(block).find('li').get().reverse()); $(block).find('.sonlist').remove(); $(block).append('