// ==UserScript== // @name Bilibili - 在未登录的情况下照常加载评论 // @namespace https://bilibili.com/ // @version 4.6 // @description 在未登录的情况下照常加载评论 | V4.6 补充对获取评论数据时接口返回-400错误的处理 // @license GPL-3.0 // @author DD1969 // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/bangumi/play/* // @match https://t.bilibili.com/* // @match https://www.bilibili.com/opus/* // @match https://space.bilibili.com/* // @match https://www.bilibili.com/read/cv* // @match https://www.bilibili.com/festival* // @match https://www.bilibili.com/list/* // @icon https://www.bilibili.com/favicon.ico // @require https://update.greasyfork.icu/scripts/510239/1454424/viewer.js // @require https://update.greasyfork.icu/scripts/475332/1250588/spark-md5.js // @require https://update.greasyfork.icu/scripts/512574/1464548/inject-bilibili-comment-style.js // @require https://update.greasyfork.icu/scripts/512576/1464552/inject-viewerjs-style.js // @grant unsafeWindow // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @downloadURL none // ==/UserScript== (async function() { 'use strict'; // no need to continue this script if user already logged in if (document.cookie.includes('DedeUserID')) return; // patch for 'unsafeWindow is not defined' const global = typeof unsafeWindow === 'undefined' ? window : unsafeWindow; // initialize options const options = { // 使用分页加载主评论 enableReplyPagination: GM_getValue('enableReplyPagination', false), // 一次性加载所有子评论 enableLoadAllSubRepliesAtOnce: GM_getValue('enableLoadAllSubRepliesAtOnce', false), // 显示用户头像边框 enableAvatarPendent: GM_getValue('enableAvatarPendent', true), // 显示评论右上角的大航海装饰 enableSailingDecoration: GM_getValue('enableSailingDecoration', true), // 显示"笔记"前缀 enableNotePrefix: GM_getValue('enableNotePrefix', true), // 显示"热评"标签 enableHotTag: GM_getValue('enableHotTag', true), // 显示"UP主觉得很赞"标签 enableLikedTag: GM_getValue('enableLikedTag', true), // 显示大会员用户名的颜色为粉色 enableVipUserNameColor: GM_getValue('enableVipUserNameColor', true), // 启用关键字搜索链接 enableKeywordSearchLink: GM_getValue('enableKeywordSearchLink', true), } // RegExp const videoRE = /https:\/\/www\.bilibili\.com\/video\/.*/; const bangumiRE = /https:\/\/www.bilibili.com\/bangumi\/play\/.*/; const dynamicRE = /https:\/\/t.bilibili.com\/\d+/; const opusRE = /https:\/\/www.bilibili.com\/opus\/\d+/; const spaceRE = /https:\/\/space.bilibili.com\/\d+/; const articleRE = /https:\/\/www.bilibili.com\/read\/cv\d+.*/; const festivalRE = /https:\/\/www.bilibili.com\/festival\/.*/; const listRE = /https:\/\/www.bilibili.com\/list\/.*/; // essential data or elements let oid, createrID, commentType, replyList; // define sort types const sortTypeConstant = { LATEST: 0, HOT: 2 }; let currentSortType; // offset data for time sort let timeSortOffsets; // use to prevent loading duplicated main reply let replyPool; // ---------- variables above ---------- // make comment buttons in dynamic page do their job if (spaceRE.test(global.location.href)) { setupCommentBtnModifier(); return; } // add style patch addStyle(); // setup video change handler setupVideoChangeHandler(); // setup setting panel & entry setupSettingPanel(); setupSettingPanelEntry(); // start loading comments start(); // ---------- functions below ---------- async function start() { // initialize oid = createrID = commentType = replyList = undefined; replyPool = {}; currentSortType = sortTypeConstant.HOT; // setup standard comment container await setupStandardCommentContainer(); // collect essential data or elements await new Promise(resolve => { const timer = setInterval(async () => { // collect oid, createrID, commentType if (videoRE.test(global.location.href)) { const videoID = global.location.pathname.replace('/video/', '').replace('/', ''); if (videoID.startsWith('av')) oid = videoID.slice(2); if (videoID.startsWith('BV')) oid = b2a(videoID); createrID = global?.__INITIAL_STATE__?.upData?.mid; commentType = 1; } else if (bangumiRE.test(global.location.href)) { oid = b2a(document.querySelector('[class*=mediainfo_mediaDesc] a[href*="video/BV"]')?.textContent); createrID = document.querySelector('a[class*=upinfo_upLink]')?.href?.split('/').filter(item => !!item).pop() || -1; commentType = 1 } else if (dynamicRE.test(global.location.href)) { const dynamicID = global.location.pathname.replace('/', ''); const dynamicDetail = await fetch(`https://api.bilibili.com/x/polymer/web-dynamic/v1/detail?id=${dynamicID}`).then(res => res.json()); oid = dynamicDetail?.data?.item?.basic?.comment_id_str; commentType = dynamicDetail?.data?.item?.basic?.comment_type; createrID = dynamicDetail?.data?.item?.modules?.module_author?.mid; } else if (opusRE.test(global.location.href)) { oid = global?.__INITIAL_STATE__?.detail?.basic?.comment_id_str; createrID = global?.__INITIAL_STATE__?.detail?.basic?.uid; commentType = global?.__INITIAL_STATE__?.detail?.basic?.comment_type; // should be '11' } else if (articleRE.test(global.location.href)) { oid = global?.__INITIAL_STATE__?.cvid; createrID = global?.__INITIAL_STATE__?.readInfo?.author?.mid; commentType = 12; } else if (festivalRE.test(global.location.href)) { oid = global?.__INITIAL_STATE__?.videoInfo?.aid; createrID = global?.__INITIAL_STATE__?.videoInfo?.upMid; commentType = 1; } else if (listRE.test(global.location.href)) { oid = new URLSearchParams(global.location.search).get('oid'); createrID = global?.__INITIAL_STATE__?.upInfo?.mid; commentType = 1; } // get reply container replyList = document.querySelector('.reply-list'); // final check if (oid && createrID && commentType && replyList) { createrID = parseInt(createrID); clearInterval(timer); resolve(); } }, 200); }); // enable switching sort type await enableSwitchingSortType(); // load first pagination await loadFirstPagination(); } async function setupStandardCommentContainer() { const container = await new Promise(resolve => { const timer = setInterval(() => { const standardContainer = document.querySelector('.comment-container'); const outdatedContainer = document.querySelector('.comment-wrapper .common'); const shadowRootContainer = document.querySelector('bili-comments'); const container = standardContainer || outdatedContainer || shadowRootContainer; if (container) { clearInterval(timer); resolve(container); } }, 200); }); // modify non-standard comment container if (!container.classList.contains('comment-container')) { container.parentElement.innerHTML = `
`; } } async function enableSwitchingSortType() { // collect elements const navSortElement = document.querySelector('.comment-container .reply-header .nav-sort'); const hotSortElement = navSortElement.querySelector('.hot-sort'); const timeSortElement = navSortElement.querySelector('.time-sort'); // reset classes navSortElement.classList.add('hot'); navSortElement.classList.remove('time'); // setup click event listener hotSortElement.addEventListener('click', () => { if (currentSortType === sortTypeConstant.HOT) return; currentSortType = sortTypeConstant.HOT; navSortElement.classList.add('hot'); navSortElement.classList.remove('time'); loadFirstPagination(); }); timeSortElement.addEventListener('click', () => { if (currentSortType === sortTypeConstant.LATEST) return; currentSortType = sortTypeConstant.LATEST; navSortElement.classList.add('time'); navSortElement.classList.remove('hot'); loadFirstPagination(); }); } async function loadFirstPagination() { // reset offset data timeSortOffsets = { 1: `{"offset":""}` }; // get data of first pagination const { data: firstPaginationData, code: resultCode } = await getPaginationData(1); // make sure 'replyList' exists await new Promise(resolve => { const timer = setInterval(() => { if (document.body.contains(replyList)) { clearInterval(timer); resolve(); } else { replyList = document.querySelector('.reply-list'); } }, 200); }); // clear replyList replyList.innerHTML = ''; // clear replyPool replyPool = {}; // script ends here if not able to fetch pagination data if (resultCode !== 0) { // ref: BV12r4y147Bj const info = resultCode === 12061 ? 'UP主已关闭评论区' : '无法从API获取评论数据'; replyList.innerHTML = `

${info}

`; return; } // load reply count const totalReplyElement = document.querySelector('.comment-container .reply-header .total-reply'); const totalReplyCount = parseInt(firstPaginationData?.cursor?.all_count) || 0; totalReplyElement.textContent = totalReplyCount; if (totalReplyCount === 0) replyList.innerHTML = '

没有更多评论

'; // check whether replies are selected // ref: BV1Dy2mY3EGy if (firstPaginationData?.cursor?.name?.includes('精选')) { const navSortElement = document.querySelector('.comment-container .reply-header .nav-sort'); navSortElement.innerHTML = `
精选评论
`; } // load the top reply if it exists if (firstPaginationData.top_replies && firstPaginationData.top_replies.length !== 0) { const topReplyData = firstPaginationData.top_replies[0]; appendReplyItem(topReplyData, true); } // script ends here if there is no reply of this video if (firstPaginationData.replies.length === 0) return; // load normal replies for (const replyData of firstPaginationData.replies) { appendReplyItem(replyData); } // add page loader options.enableReplyPagination ? addReplyPageSwitcher() : addAnchor(); } async function getPaginationData(paginationNumber) { const params = { oid, type: commentType, wts: parseInt(Date.now() / 1000) }; if (currentSortType === sortTypeConstant.HOT) { params.mode = 3; params.pagination_str = `{"offset":"{\\"type\\":1,\\"data\\":{\\"pn\\":${paginationNumber}}}"}`; return await fetch(`https://api.bilibili.com/x/v2/reply/wbi/main?${await getWbiQueryString(params)}`).then(res => res.json()); } if (currentSortType === sortTypeConstant.LATEST) { params.mode = 2; params.pagination_str = timeSortOffsets[paginationNumber]; const fetchResult = await fetch(`https://api.bilibili.com/x/v2/reply/wbi/main?${await getWbiQueryString(params)}`).then(res => res.json()); // prepare offset data of next pagination if (fetchResult.code === 0) { const nextOffset = fetchResult.data.cursor.pagination_reply.next_offset; const cursor = nextOffset ? JSON.parse(nextOffset).Data.cursor : -1; timeSortOffsets[paginationNumber + 1] = `{"offset":"{\\"type\\":3,\\"data\\":{\\"cursor\\":${cursor}}}"}`; } else { fetchResult.data = fetchResult.data || {}; } return fetchResult; } } function appendReplyItem(replyData, isTopReply) { if (!options.enableReplyPagination && replyPool[replyData.rpid_str]) return; const replyItemElement = document.createElement('div'); replyItemElement.classList.add('reply-item'); replyItemElement.innerHTML = `
${ replyData.member.pendant.image ? `
` : '' }
${ replyData.member.user_sailing?.cardbg ? `
NO.
${replyData.member.user_sailing.cardbg.fan.number.toString().padStart(6, '0')}
` : '' }
${isTopReply? '置顶': ''}${replyData.content.pictures ? `
笔记
` : ''}${getConvertedMessage(replyData.content)}
${ replyData.content.pictures ? `
${getImageItems(replyData.content.pictures)}
` : '' }
${getFormattedTime(replyData.ctime)} ${replyData.like} 回复
${ replyData.card_label ? replyData.card_label.reduce((acc, cur) => acc + `${cur.text_content}`, '') : '' }
${getSubReplyItems(replyData.replies) || ''} ${ replyData.rcount > 3 ? `
共${replyData.rcount}条回复, 点击查看
` : '' }
`; replyList.appendChild(replyItemElement); if (!options.enableReplyPagination) replyPool[replyData.rpid_str] = true; // setup image viewer const previewImageContainer = replyItemElement.querySelector('.preview-image-container'); if (previewImageContainer) new Viewer(previewImageContainer, { title: false, toolbar: false, tooltip: false, keyboard: false }); // setup view more button const subReplyList = replyItemElement.querySelector('.sub-reply-list'); const viewMoreBtn = replyItemElement.querySelector('.view-more-btn'); viewMoreBtn && viewMoreBtn.addEventListener('click', () => { options.enableLoadAllSubRepliesAtOnce ? loadAllSubReplies(replyData.rpid, subReplyList) : loadPaginatedSubReplies(replyData.rpid, subReplyList, replyData.rcount, 1); }); } function getFormattedTime(ms) { const time = new Date(ms * 1000); const year = time.getFullYear(); const month = (time.getMonth() + 1).toString().padStart(2, '0'); const day = time.getDate().toString().padStart(2, '0'); const hour = time.getHours().toString().padStart(2, '0'); const minute = time.getMinutes().toString().padStart(2, '0'); return `${year}-${month}-${day} ${hour}:${minute}`; } function getMemberLevelColor(level) { return ({ 0: '#C0C0C0', 1: '#BBBBBB', 2: '#8BD29B', 3: '#7BCDEF', 4: '#FEBB8B', 5: '#EE672A', 6: '#F04C49' })[level]; } function getConvertedMessage(content) { let result = content.message; // built blacklist of keyword, to avoid being converted to link incorrectly const keywordBlacklist = ['https://www.bilibili.com/video/av', 'https://b23.tv/mall-']; // convert vote to link if (content.vote && content.vote.deleted === false) { const linkElementHTML = `${content.vote.title}`; keywordBlacklist.push(linkElementHTML); result = result.replace(`{vote:${content.vote.id}}`, linkElementHTML); } // convert emote tag to image if (content.emote) { for (const [key, value] of Object.entries(content.emote)) { const imageElementHTML = `${key}`; keywordBlacklist.push(imageElementHTML); result = result.replaceAll(key, imageElementHTML); } } // convert timestamp to link result = result.replaceAll(/(\d{1,2}[::]){1,2}\d{1,2}/g, (timestamp) => { timestamp = timestamp.replaceAll(':', ':'); // return plain text if no video in page if(!(videoRE.test(global.location.href) || bangumiRE.test(global.location.href) || festivalRE.test(global.location.href) || listRE.test(global.location.href))) return timestamp; const parts = timestamp.split(':'); // return plain text if any part of timestamp equal to or bigger than 60 if (parts.some(part => parseInt(part) >= 60)) return timestamp; let totalSecond; if (parts.length === 2) totalSecond = parseInt(parts[0]) * 60 + parseInt(parts[1]); else if (parts.length === 3) totalSecond = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]); // return plain text if failed to get vaild number of second if (Number.isNaN(totalSecond)) return timestamp; const linkElementHTML = `${timestamp}` keywordBlacklist.push(linkElementHTML); return linkElementHTML; }); // convert @ user if (content.at_name_to_mid) { for (const [key, value] of Object.entries(content.at_name_to_mid)) { const linkElementHTML = `@${key}`; keywordBlacklist.push(linkElementHTML); result = result.replaceAll(`@${key}`, linkElementHTML); } } // convert url to link if (Object.keys(content.jump_url).length) { // make sure links are converted first const entries = [].concat( Object.entries(content.jump_url).filter(entry => entry[0].startsWith('https://')), Object.entries(content.jump_url).filter(entry => !entry[0].startsWith('https://')) ); for (const [key, value] of entries) { const href = (key.startsWith('BV') || /^av\d+$/.test(key)) ? `https://www.bilibili.com/video/${key}` : (value.pc_url || key); if (href.includes('search.bilibili.com') && (!options.enableKeywordSearchLink || keywordBlacklist.join('').includes(key))) continue; const linkElementHTML = `${value.title}`; keywordBlacklist.push(linkElementHTML); result = result.replaceAll(key, linkElementHTML); } } return result; } function getImageItems(images) { let imageSizeConfig = 'width: 96px; height: 96px;'; if (images.length === 1) imageSizeConfig = 'max-width: 280px; max-height: 180px;'; if (images.length === 2) imageSizeConfig = 'width: 128px; height: 128px;'; let result = ''; for (const image of images) { result += `
`; } return result; } function getSubReplyItems(subReplies) { if (!subReplies || subReplies.length === 0) return; let result = ''; for (const replyData of subReplies) { result += `
${replyData.member.uname} LV${replyData.member.level_info.current_level} ${ createrID === replyData.mid ? `` : '' }
${getConvertedMessage(replyData.content)}
${getFormattedTime(replyData.ctime)} ${replyData.like} 回复
`; } return result; } async function loadAllSubReplies(rootReplyID, subReplyList) { let subPaginationCounter = 1; while(true) { const subReplyData = await fetch(`https://api.bilibili.com/x/v2/reply/reply?oid=${oid}&pn=${subPaginationCounter++}&ps=20&root=${rootReplyID}&type=${commentType}`).then(res => res.json()).then(json => json.data); if (subPaginationCounter - 1 === 1) subReplyList.innerHTML = ''; if (subReplyData.replies && subReplyData.replies.length > 0) { subReplyList.innerHTML += getSubReplyItems(subReplyData.replies); await new Promise(resolve => setTimeout(resolve, 1000)); } else { break; } } } async function loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, paginationNumber) { // replace reply list with new replies const subReplyData = await fetch(`https://api.bilibili.com/x/v2/reply/reply?oid=${oid}&pn=${paginationNumber}&ps=10&root=${rootReplyID}&type=${commentType}`).then(res => res.json()).then(json => json.data); if (subReplyData.replies) subReplyList.innerHTML = getSubReplyItems(subReplyData.replies); // add page switcher addSubReplyPageSwitcher(rootReplyID, subReplyList, subReplyAmount, paginationNumber); // scroll to the top of replyItem const replyItem = subReplyList.parentElement.parentElement; replyItem.scrollIntoView({ behavior: 'instant' }); // scroll up a bit more because of the fixed header global.scrollTo(0, document.documentElement.scrollTop - 60); } function addSubReplyPageSwitcher(rootReplyID, subReplyList, subReplyAmount, currentPageNumber) { if (subReplyAmount <= 10) return; const pageAmount = Math.ceil(subReplyAmount / 10); const pageSwitcher = document.createElement('div'); pageSwitcher.classList.add('view-more'); pageSwitcher.innerHTML = `
共${pageAmount}页 ${ currentPageNumber !== 1 ? '上一页' : '' } ${ (() => { // 4 on the left, 4 on the right, then merge const left = [currentPageNumber - 4, currentPageNumber - 3, currentPageNumber - 2, currentPageNumber - 1].filter(num => num >= 1); const right = [currentPageNumber + 1, currentPageNumber + 2, currentPageNumber + 3, currentPageNumber + 4].filter(num => num <= pageAmount); const merge = [].concat(left, currentPageNumber, right); // chosen 5(if able) let chosen; if (currentPageNumber <= 3) chosen = merge.slice(0, 5); else if (currentPageNumber >= pageAmount - 3) chosen = merge.reverse().slice(0, 5).reverse(); else chosen = merge.slice(merge.indexOf(currentPageNumber) - 2, merge.indexOf(currentPageNumber) + 3); // add first and dots let final = JSON.parse(JSON.stringify(chosen)); if (!final.includes(1)) { let front = [1]; if (final.at(0) !== 2) front = [1, '...']; final = [].concat(front, final); } // add last and dots if (!final.includes(pageAmount)) { let back = [pageAmount]; if (final.at(-1) !== pageAmount - 1) back = ['...', pageAmount]; final = [].concat(final, back); } // assemble to html return final.reduce((acc, cur) => { if (cur === '...') return acc + '...'; if (cur === currentPageNumber) return acc + `${cur}`; return acc + `${cur}`; }, ''); })() } ${ currentPageNumber !== pageAmount ? '下一页': '' }
`; // add click event listener pageSwitcher.querySelector('.pagination-to-prev-btn')?.addEventListener('click', () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, currentPageNumber - 1)); pageSwitcher.querySelector('.pagination-to-next-btn')?.addEventListener('click', () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, currentPageNumber + 1)); pageSwitcher.querySelectorAll('.pagination-page-number:not(.current-page)')?.forEach(pageNumberElement => { const number = parseInt(pageNumberElement.textContent); pageNumberElement.addEventListener('click', () => loadPaginatedSubReplies(rootReplyID, subReplyList, subReplyAmount, number)); }); // append page switcher subReplyList.appendChild(pageSwitcher); } async function addReplyPageSwitcher() { // clear old page switcher const oldPageSwitcher = document.querySelector('.comment-container .reply-warp .page-switcher'); oldPageSwitcher && oldPageSwitcher.remove(); let isPageAmountFound = false; let currentMaxPageNumber = 1; let currentPageNumber = 1; // check if there is no reply in page 2 const { data: nextPaginationData } = await getPaginationData(currentPageNumber + 1); if (!nextPaginationData.replies || nextPaginationData.replies.length === 0) return; const pageSwitcher = document.createElement('div'); pageSwitcher.classList.add('page-switcher'); pageSwitcher.style = ` width: 100%; display: flex; justify-content: center; transform: translateY(-60px); `; pageSwitcher.appendChild(generatePageSwitcher()); document.querySelector('.comment-container .reply-warp').appendChild(pageSwitcher); function generatePageSwitcher() { const wrapper = document.createElement('div'); wrapper.classList.add('page-switcher-wrapper'); wrapper.innerHTML = ` ${ currentPageNumber === 1 ? '上一页' : '上一页' } ${ (() => { // 4 on the left, 4 on the right, then merge const left = [currentPageNumber - 4, currentPageNumber - 3, currentPageNumber - 2, currentPageNumber - 1].filter(num => num >= 1); const right = [currentPageNumber + 1, currentPageNumber + 2, currentPageNumber + 3, currentPageNumber + 4].filter(num => num <= currentMaxPageNumber); const merge = [].concat(left, currentPageNumber, right); // chosen 5(if able) let chosen; if (currentPageNumber <= 3) chosen = merge.slice(0, 5); else if (currentPageNumber >= currentMaxPageNumber - 3) chosen = merge.reverse().slice(0, 5).reverse(); else chosen = merge.slice(merge.indexOf(currentPageNumber) - 2, merge.indexOf(currentPageNumber) + 3); // add first and dots let final = JSON.parse(JSON.stringify(chosen)); if (!final.includes(1)) { let front = [1]; if (final.at(0) !== 2) front = [1, 'dot']; final = [].concat(front, final); } // add last and dots if (!final.includes(currentMaxPageNumber)) { let back = [currentMaxPageNumber]; if (final.at(-1) !== currentMaxPageNumber - 1) back = ['dot', currentMaxPageNumber]; final = [].concat(final, back); } // assemble to html return final.reduce((acc, cur) => { if (cur === 'dot') return acc + '•••'; if (cur === currentPageNumber) return acc + `${cur}`; return acc + `${cur}`; }, ''); })() } ${ isPageAmountFound && currentPageNumber === currentMaxPageNumber ? '下一页' : '下一页' } `; // prev button click event wrapper.querySelector('.page-switcher-prev-btn')?.addEventListener('click', async () => { currentPageNumber -= 1; const { data: prevPaginationData } = await getPaginationData(currentPageNumber); replyList.innerHTML = ''; // if loading page 1, load top reply if it exists if (currentPageNumber === 1 && prevPaginationData.top_replies && prevPaginationData.top_replies.length !== 0) { const topReplyData = prevPaginationData.top_replies[0]; appendReplyItem(topReplyData, true); } for (const replyData of prevPaginationData.replies) { appendReplyItem(replyData); } pageSwitcher.innerHTML = ''; pageSwitcher.appendChild(generatePageSwitcher()); scrollToTopOfReplyList(); }); // next button click event wrapper.querySelector('.page-switcher-next-btn')?.addEventListener('click', async function nextButtonOnClickHandler(e) { if (currentPageNumber === currentMaxPageNumber && isPageAmountFound) return; const { data: nextPaginationData } = await getPaginationData(currentPageNumber + 1); if (!nextPaginationData.replies || nextPaginationData.replies.length === 0) { isPageAmountFound = true; e.target.classList.add('page-switcher-next-btn__disabled'); e.target.classList.remove('page-switcher-next-btn'); return; } if (currentPageNumber === currentMaxPageNumber) currentMaxPageNumber += 1; currentPageNumber += 1; replyList.innerHTML = ''; for (const replyData of nextPaginationData.replies) { appendReplyItem(replyData); } pageSwitcher.innerHTML = ''; pageSwitcher.appendChild(generatePageSwitcher()); scrollToTopOfReplyList(); }); // number button click event wrapper.querySelectorAll('.page-switcher-number:not(.page-switcher-current-page)')?.forEach(numberElement => { numberElement.addEventListener('click', async () => { const targetPageNumber = parseInt(numberElement.textContent); currentPageNumber = targetPageNumber; const { data: paginationData } = await getPaginationData(targetPageNumber); replyList.innerHTML = ''; // if loading page 1, load top reply if it exists if (targetPageNumber === 1 && paginationData.top_replies && paginationData.top_replies.length !== 0) { const topReplyData = paginationData.top_replies[0]; appendReplyItem(topReplyData, true); } for (const replyData of paginationData.replies) { appendReplyItem(replyData); } pageSwitcher.innerHTML = ''; pageSwitcher.appendChild(generatePageSwitcher()); scrollToTopOfReplyList(); }); }); return wrapper; } // scroll to the top of reply list function scrollToTopOfReplyList() { replyList.scrollIntoView({ behavior: 'instant' }); // scroll up a bit more global.scrollTo(0, document.documentElement.scrollTop - 196); } } function addAnchor() { // clear old anchor const oldAnchor = document.querySelector('.comment-container .reply-warp .anchor-for-loading'); oldAnchor && oldAnchor.remove(); const anchorElement = document.createElement('div'); anchorElement.classList.add('anchor-for-loading'); anchorElement.textContent = '正在加载...'; anchorElement.style = ` width: calc(100% - 22px); height: 40px; margin-left: 22px; display: flex; justify-content: center; align-items: center; transform: translateY(-60px); color: #61666d; `; document.querySelector('.comment-container .reply-warp').appendChild(anchorElement); let paginationCounter = 1; const ob = new IntersectionObserver(async (entries) => { if (!entries[0].isIntersecting) return; const { data: newPaginationData } = await getPaginationData(++paginationCounter); if (!newPaginationData.replies || newPaginationData.replies.length === 0) { anchorElement.textContent = '所有评论已加载完毕'; ob.disconnect(); return; } for (const replyData of newPaginationData.replies) { appendReplyItem(replyData); } }); ob.observe(anchorElement); } // bvid to aid, ref: https://greasyfork.org/scripts/394296 function b2a(bvid) { const XOR_CODE = 23442827791579n; const MASK_CODE = 2251799813685247n; const BASE = 58n; const BYTES = ["B", "V", 1, "", "", "", "", "", "", "", "", ""]; const BV_LEN = BYTES.length; const ALPHABET = 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf'.split(''); const DIGIT_MAP = [0, 1, 2, 9, 7, 5, 6, 4, 8, 3, 10, 11]; let r = 0n; for (let i = 3; i < BV_LEN; i++) { r = r * BASE + BigInt(ALPHABET.indexOf(bvid[DIGIT_MAP[i]])); } return `${r & MASK_CODE ^ XOR_CODE}`; } // ref: https://socialsisteryi.github.io/bilibili-API-collect/docs/misc/sign/wbi.html async function getWbiQueryString(params) { // get origin key const { img_url, sub_url } = await fetch('https://api.bilibili.com/x/web-interface/nav').then(res => res.json()).then(json => json.data.wbi_img); const imgKey = img_url.slice(img_url.lastIndexOf('/') + 1, img_url.lastIndexOf('.')); const subKey = sub_url.slice(sub_url.lastIndexOf('/') + 1, sub_url.lastIndexOf('.')); const originKey = imgKey + subKey; // get mixin key const mixinKeyEncryptTable = [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52 ]; const mixinKey = mixinKeyEncryptTable.map(n => originKey[n]).join('').slice(0, 32); // generate basic query string const query = Object .keys(params) .sort() // sort properties by key .map(key => { const value = params[key].toString().replace(/[!'()*]/g, ''); // remove characters !'()* in value return `${encodeURIComponent(key)}=${encodeURIComponent(value)}` }) .join('&'); // calculate wbi sign const wbiSign = SparkMD5.hash(query + mixinKey); return query + '&w_rid=' + wbiSign; } function setupCommentBtnModifier() { setInterval(() => { const dynItems = document.querySelectorAll('.bili-dyn-list .bili-dyn-item'); dynItems.forEach(dynItem => { if (dynItem.classList.contains('comment-btn-modified')) return; const dynContentElement = dynItem.querySelector('.bili-dyn-item__body div[data-module=desc]') || dynItem.querySelector('.bili-dyn-item__body a.bili-dyn-card-video'); const commentBtnElement = dynItem.querySelector('.bili-dyn-item__footer .bili-dyn-action.comment'); if (dynContentElement && commentBtnElement) { commentBtnElement.onclick = () => dynContentElement.click(); dynItem.classList.add('comment-btn-modified'); } }); }, 1000); } function addStyle() { // optional CSS const optionalCSS = document.createElement('style'); if (!options.enableAvatarPendent) optionalCSS.textContent += `.bili-avatar-pendent-dom { display: none !important; } `; if (!options.enableSailingDecoration) optionalCSS.textContent += `.reply-decorate { display: none !important; } `; if (!options.enableNotePrefix) optionalCSS.textContent += `.note-prefix { display: none !important; } `; if (!options.enableHotTag) optionalCSS.textContent += `.reply-tag-hot { display: none !important; } `; if (!options.enableLikedTag) optionalCSS.textContent += `.reply-tag-liked { display: none !important; } `; if (!options.enableVipUserNameColor) optionalCSS.textContent += `.user-name, .sub-user-name { color: #61666d !important; } `; document.head.appendChild(optionalCSS); // avatar CSS const avatarCSS = document.createElement('style'); avatarCSS.textContent = ` .reply-item .root-reply-avatar .avatar .bili-avatar { width: 48px; height: 48px; } .sub-reply-item .sub-reply-avatar .avatar .bili-avatar { width: 30px; height: 30px; } @media screen and (max-width: 1620px) { .reply-item .root-reply-avatar .avatar .bili-avatar { width: 40px; height: 40px; } .sub-reply-item .sub-reply-avatar .avatar .bili-avatar { width: 24px; height: 24px; } } `; document.head.appendChild(avatarCSS); // view-more CSS const viewMoreCSS = document.createElement('style'); viewMoreCSS.textContent = ` .sub-reply-container .view-more-btn:hover { color: #00AEEC; } .view-more { padding-left: 8px; color: #222; font-size: 13px; user-select: none; } .pagination-page-count { margin-right: 10px; } .pagination-page-dot, .pagination-page-number { margin: 0 4px; } .pagination-btn, .pagination-page-number { cursor: pointer; } .current-page, .pagination-btn:hover, .pagination-page-number:hover { color: #00AEEC; } `; document.head.appendChild(viewMoreCSS); // page switcher CSS const pageSwitcherCSS = document.createElement('style'); pageSwitcherCSS.textContent = ` .page-switcher-wrapper { display: flex; font-size: 14px; color: #666; user-select: none; } .page-switcher-wrapper span { margin-right: 6px; } .page-switcher-wrapper span:not(.page-switcher-dot){ display: flex; padding: 0 14px; height: 38px; align-items: center; border: 1px solid #D7DDE4; border-radius: 4px; cursor: pointer; transition: border-color 0.2s; } .page-switcher-prev-btn:hover, .page-switcher-next-btn:hover, .page-switcher-number:hover { border-color: #00A1D6 !important; } .page-switcher-current-page { color: white; background-color: #00A1D6; border-color: #00A1D6 !important; } .page-switcher-dot { padding: 0 5px; display: flex; align-items: center; color: #CCC; } .page-switcher-prev-btn__disabled, .page-switcher-next-btn__disabled { color: #D7DDE4 !important; cursor: not-allowed !important; } `; document.head.appendChild(pageSwitcherCSS); // add other CSS const otherCSS = document.createElement('style'); otherCSS.textContent = ` .jump-link { color: #008DDA; } .login-tip, .fixed-reply-box, .v-popover:has(.login-panel-popover) { display: none; } `; document.head.appendChild(otherCSS); // dynamic page CSS if (dynamicRE.test(global.location.href) || opusRE.test(global.location.href)) { const dynPageCSS = document.createElement('style'); dynPageCSS.textContent = ` #app .opus-detail { min-width: 960px; } #app .opus-detail .right-sidebar-wrap { margin-left: 980px !important; transition: none; } #app > .content { min-width: 960px; } .v-popover:has(.login-panel-popover), .fixed-reply-box, .login-tip { display: none; } .note-prefix { fill: #BBBBBB; } .bili-comment-container svg { fill: inherit !important; } `; document.head.appendChild(dynPageCSS); } // article & festival page CSS if (articleRE.test(global.location.href) || festivalRE.test(global.location.href)) { const miscCSS = document.createElement('style'); miscCSS.textContent = ` :root { --text1: #18191C; --text3: #9499A0; --brand_pink: #FF6699; --graph_bg_thick: #e3e5e7; } .page-switcher { margin-top: 40px; } .van-popover:has(.unlogin-popover) { display: none !important; } `; document.head.appendChild(miscCSS); } } function setupVideoChangeHandler() { // redirect to new page if video source changed in festival page if (festivalRE.test(global.location.href)) { let record; const getBVID = () => global?.__INITIAL_STATE__?.videoInfo?.bvid; setInterval(() => { if (!record) record = getBVID(); else if (record !== getBVID()) global.location.href = `${global.location.origin}${global.location.pathname}?bvid=${getBVID()}`; }, 1000); } // load new comment module when url changed if (videoRE.test(global.location.href) || bangumiRE.test(global.location.href) || listRE.test(global.location.href)) { const getHref = () => { const p = new URLSearchParams(global.location.search).get('p'); const oid = new URLSearchParams(global.location.search).get('oid'); return global.location.origin + global.location.pathname + (p ? `?p=${p}` : '') + (oid ? `?oid=${oid}` : ''); } let oldHref = getHref(); setInterval(() => { const newHref = getHref(); if (oldHref !== newHref) { oldHref = newHref; start(); } }, 1000); } } function setupSettingPanel() { // CSS const settingPanelCSS = document.createElement('style'); settingPanelCSS.textContent = ` #setting-panel-container { position: fixed; top: 0; left: 0; z-index: 999999999; width: 100vw; height: 100vh; display: none; flex-direction: column; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.5); } .setting-panel-wrapper { width: 600px; padding: 16px; display: flex; flex-direction: column; background-color: #FFFFFF; border-radius: 8px; user-select: none; } .setting-panel-title { margin-top: 0; margin-bottom: 8px; padding-top: 16px; padding-left: 12px; font-size: 28px; } .setting-panel-option-group { display: flex; flex-direction: column; width: 100%; font-size: 16px; } .setting-panel-option-item { padding: 16px 16px; display: flex; justify-content: space-between; align-items: center; border-radius: 4px; } .setting-panel-option-item:hover { background-color: #FAFAFA; } .setting-panel-option-item-switch { display: flex; align-items: center; width: 40px; height: 20px; padding: 2px; cursor: pointer; border-radius: 4px; } .setting-panel-option-item-switch[data-status="off"] { justify-content: flex-start; background-color: #CCCCCC; } .setting-panel-option-item-switch[data-status="on"] { justify-content: flex-end; background-color: #00AEEC; } .setting-panel-option-item-switch:after { content: ''; width: 20px; height: 20px; background-color: #FFFFFF; border-radius: 4px; } #setting-panel-close-btn { margin-top: 16px; padding: 2px; width: 20px; height: 20px; display: flex; justify-content: center; align-items: center; font-size: 20px; color: #FFFFFF; border: 2px solid #FFFFFF; border-radius: 100%; cursor: pointer; user-select: none; } `; document.head.appendChild(settingPanelCSS); // HTML const containerElement = document.createElement('div'); containerElement.id = 'setting-panel-container'; containerElement.innerHTML = `

自定义设置

使用分页加载主评论
一次性加载所有子评论
显示用户头像边框
显示评论右上角的大航海装饰
显示"笔记"前缀
显示"热评"标签
显示"UP主觉得很赞"标签
显示大会员用户名的颜色为粉色
启用关键字搜索链接
⚠️ 所有改动将在页面刷新后生效
× `; // click events containerElement.querySelectorAll('.setting-panel-option-item-switch').forEach(switchElement => { switchElement.onclick = function(e) { const { key, status } = this.dataset; this.dataset.status = status === 'off' ? 'on' : 'off'; GM_setValue(key, this.dataset.status === 'on'); } }); containerElement.querySelector('#setting-panel-close-btn').onclick = () => containerElement.style.display = 'none'; // append to document document.body.appendChild(containerElement); } function setupSettingPanelEntry() { const settingPanelElement = document.querySelector('#setting-panel-container'); const showSettingPanel = () => settingPanelElement.style.display = 'flex'; // entry 1: gear icon setInterval(() => { const avatarElement = document.querySelector('.comment-container .reply-box .bili-avatar:not(.modified)') || document.querySelector('.comment-container .comment-send .bili-avatar:not(.modified)'); if (avatarElement) { const gearElement = document.createElement('span'); gearElement.id = 'open-setting-panel-btn'; gearElement.innerHTML = ''; gearElement.style = ` width: 48px; height: 48px; display: flex; justify-content: center; align-items: center; background-color: #F1F1F1; border-radius: 100%; border: 1px solid #DEDEDE; font-size: 48px; cursor: pointer; user-select: none; `; gearElement.onclick = showSettingPanel; avatarElement.innerHTML = ''; avatarElement.style = ` display: flex; justify-content: center; align-items: center; background-image: none !important; `; avatarElement.classList.add('modified'); avatarElement.appendChild(gearElement); } }, 1000); // entry 2: menu command GM_registerMenuCommand("自定义设置", showSettingPanel); } })();