// ==UserScript==
// @name Bilibili - 在未登录的情况下照常加载评论
// @namespace https://bilibili.com/
// @version 3.8
// @description 在未登录的情况下照常加载评论 | V3.8 修复动态页面无法加载评论的问题
// @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://mirrors.sustech.edu.cn/cdnjs/ajax/libs/viewerjs/1.11.6/viewer.min.js
// @require https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/spark-md5/3.0.2/spark-md5.min.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
await 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('a[class*=mediainfo_avLink]')?.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('.bili-comment > .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')) {
const commentCount = await new Promise(resolve => {
const timer = setInterval(() => {
const outdatedCommentCount = document.querySelector('.comment-wrapper .b-head .b-head-t.results')?.textContent;
const dynamicSideToolbarCommentCount = document.querySelector('.side-toolbar > .side-toolbar__box > .comment > .side-toolbar__action__text')?.textContent;
const shadowRootCommentCount = global?.__INITIAL_STATE__?.videoData?.stat?.reply;
const filterResult = [outdatedCommentCount, dynamicSideToolbarCommentCount, shadowRootCommentCount].filter(count => !Number.isNaN(parseInt(count)));
if (filterResult.length === 1) { clearInterval(timer); resolve(filterResult[0]); }
}, 200);
});
container.parentElement.innerHTML = `
`;
}
}
async function enableSwitchingSortType() {
// collect elements
const { selectedReplyElement, hotSortElement, timeSortElement } = await new Promise(resolve => {
const timer = setInterval(() => {
const selectedReplyElement = document.querySelector('.comment-container .reply-header .nav-select-reply');
const hotSortElement = document.querySelector('.comment-container .reply-header .hot-sort');
const timeSortElement = document.querySelector('.comment-container .reply-header .time-sort');
if (selectedReplyElement || (hotSortElement && timeSortElement)) {
clearInterval(timer);
resolve({ selectedReplyElement, hotSortElement, timeSortElement });
}
}, 200);
});
// no need to setup click event listener if replies are selected
if (selectedReplyElement) return;
// reset text color
hotSortElement.style.color = '#18191C';
timeSortElement.style.color = '#9499A0';
// setup click event listener
hotSortElement.addEventListener('click', () => {
if (currentSortType === sortTypeConstant.HOT) return;
currentSortType = sortTypeConstant.HOT;
hotSortElement.style.color = '#18191C';
timeSortElement.style.color = '#9499A0';
loadFirstPagination();
});
timeSortElement.addEventListener('click', () => {
if (currentSortType === sortTypeConstant.LATEST) return;
currentSortType = sortTypeConstant.LATEST;
hotSortElement.style.color = '#9499A0';
timeSortElement.style.color = '#18191C';
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) {
replyList.innerHTML = '无法从API获取评论数据
';
return;
}
// 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
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}}}"}`;
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 emote tag to image
if (content.emote) {
for (const [key, value] of Object.entries(content.emote)) {
const imageElementHTML = ` `;
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) {
for (const [key, value] of Object.entries(content.jump_url)) {
const href = key.startsWith('BV') ? `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) {
images = images.slice(0, 3);
const imageSizeConfig = ({
1: 'max-width: 280px; max-height: 180px;',
2: 'width: 128px; height: 128px;',
3: 'width: 96px; height: 96px;',
})[images.length];
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 += `
${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
let elemTop = subReplyList.parentElement.parentElement.offsetTop;
let parentElem = subReplyList.parentElement.parentElement.offsetParent;
while (parentElem) {
elemTop += parentElem.offsetTop;
parentElem = parentElem.offsetParent;
}
global.scrollTo(0, elemTop - 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 = `
`;
// 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() {
let elemTop = replyList.offsetTop;
let parentElem = replyList.offsetParent;
while (parentElem) {
elemTop += parentElem.offsetTop;
parentElem = parentElem.offsetParent;
}
global.scrollTo(0, elemTop - 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);
}
async function addStyle() {
// base CSS modified from https://s1.hdslb.com/bfs/seed/jinkela/commentpc/comment-pc-vue.next.js
const baseCSS = document.createElement('style');
baseCSS.textContent = await fetch('https://update.greasyfork.icu/scripts/499400/bilibili-comment-style.user.css').then(res => res.text());
document.head.appendChild(baseCSS);
// viewerjs
const viewerjsCSS = document.createElement('style');
viewerjsCSS.textContent = await fetch('https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/viewerjs/1.11.6/viewer.min.css').then(res => res.text());
document.head.appendChild(viewerjsCSS);
// 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);
}
})();