Greasy Fork is available in English.
章节讨论中置顶显示自己的吐槽,高亮回复过的章节格子
当前为
// ==UserScript==
// @name 章节讨论吐槽加强
// @namespace https://bgm.tv/group/topic/408098
// @version 0.2.2
// @description 章节讨论中置顶显示自己的吐槽,高亮回复过的章节格子
// @author oo
// @include http*://bgm.tv/*
// @include http*://chii.in/*
// @include http*://bangumi.tv/*
// @license MIT
// ==/UserScript==
(async function () {
const user = document.querySelector('.avatar').href.split('/').pop();
const style = document.createElement('style');
style.textContent = `
a.load-epinfo.epBtnWatched {
opacity: .6;
}
a.load-epinfo.epBtnWatched.commented {
opacity: 1;
background: blueviolet;
}
a.load-epinfo.epBtnWatched.uncommented {
opacity: 1;
}
a.load-epinfo.epBtnAir.commented {
background: #cfa0fc;
}
html[data-theme="dark"] a.load-epinfo.epBtnWatched.commented {
background: blueviolet;
}
html[data-theme="dark"] a.load-epinfo.epBtnAir.commented {
background: #cfa0fc;
}
`;
document.head.appendChild(style);
async function getDOM(url) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('HTTP request failed');
const html = await response.text();
const dom = new DOMParser().parseFromString(html, 'text/html');
return dom;
} catch (error) {
console.error('章节讨论置顶自己的吐槽: Error fetching and parsing page:', error);
}
}
const cacheHandler = {
// 初始化时检查并清理过期项目
init(target) {
const data = JSON.parse(localStorage.getItem(target.storageKey) || '{}');
const now = Date.now();
for (const key in data) {
if (data[key].expiry < now) {
delete data[key];
}
}
localStorage.setItem(target.storageKey, JSON.stringify(data));
},
get(target, key) {
const data = JSON.parse(localStorage.getItem(target.storageKey) || '{}');
const now = Date.now();
const oneMonth = 30 * 24 * 60 * 60 * 1000;
if (data[key] && now < data[key].expiry) {
// 调用时延后一个月过期时间
data[key].expiry = now + oneMonth;
localStorage.setItem(target.storageKey, JSON.stringify(data));
return data[key].value;
} else {
delete data[key];
localStorage.setItem(target.storageKey, JSON.stringify(data));
return undefined;
}
},
set(target, key, value) {
const now = Date.now();
const oneMonth = 30 * 24 * 60 * 60 * 1000;
const expiry = now + oneMonth;
const data = JSON.parse(localStorage.getItem(target.storageKey) || '{}');
data[key] = { value, expiry };
localStorage.setItem(target.storageKey, JSON.stringify(data));
return true;
}
};
const cacheTarget = { storageKey: 'incheijs_ep_cache' };
cacheHandler.init(cacheTarget);
const cache = new Proxy(cacheTarget, cacheHandler);
// 章节讨论页
if (location.pathname.startsWith('/ep')) {
let replies = getRepliesFromDOM(document);
const id = location.pathname.split('/')[2];
if (replies[0]) {
document.getElementById('reply_wrapper').before(...replies.map(elem => {
const clone = elem.cloneNode(true);
clone.id += '_clone';
clone.classList.add('cloned_mine');
clone.querySelectorAll('.likes_grid a').forEach(a => {
a.href = 'javascript:';
a.style.cursor = 'default';
}); // 防止点击贴贴无效跳转首页
clone.style.setProperty('display', 'block', 'important'); // 兼容开播前隐藏
return clone;
}));
cache[id] = true;
} else {
cache[id] = false;
}
// 兼容开播前隐藏
document.querySelector('#comments_seperater')?.addEventListener('click', () => {
document.querySelectorAll('.cloned_mine').forEach(e => e.style.setProperty('display', 'block', 'important'));
});
// 添加回复
document.querySelector('#ReplyForm').addEventListener('submit', async () => {
const observer = new MutationObserver(() => {
// 因 AJAX 添加的元素未设置 dataset,不可用 getRepliesFromDOM
const myReplies = [...document.querySelectorAll('#comment_list .row_reply')].filter(comment => comment.querySelector('.avatar').href.split('/').pop() === user);
if (myReplies.length) {
cache[id] = true;
observer.disconnect();
}
});
observer.observe(document.querySelector('#comment_list'), { childList: true });
});
// 侧栏其他章节,无法直接判断是否看过,只取缓存不检查
const epElems = document.querySelectorAll('.sideEpList li a');
for (const elem of epElems) {
const url = elem.href;
const id = url.split('/')[4];
if (cache[id] === true) elem.style.color = 'blueviolet';
}
}
function getRepliesFromDOM(dom) {
return [...dom.querySelectorAll('#comment_list .row_reply')].filter(comment => comment.dataset.itemUser === user);
}
// 动画条目页
const subjectID = location.pathname.match(/(?<=subject\/)\d+/)?.[0];
if (subjectID) {
const type = document.querySelector('.focus').href.split('/')[3];
if (['anime', 'real'].includes(type)) {
renderChecks();
}
}
// 首页
if (location.pathname === '/') {
renderChecks();
}
async function retryAsyncOperation(operation, maxRetries = 3, delay = 1000) {
let error;
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (e) {
error = e;
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw error;
}
async function limitConcurrency(tasks, concurrency = 2) {
const results = [];
let index = 0;
async function runTask() {
while (index < tasks.length) {
const currentIndex = index++;
const task = tasks[currentIndex];
try {
const result = await task();
results[currentIndex] = result;
} catch (error) {
results[currentIndex] = error;
}
}
}
const runners = Array.from({ length: concurrency }, runTask);
await Promise.all(runners);
return results;
}
function generateCacheKey(url) {
return new URL(url).pathname.split('/').filter(part => part).pop();
}
async function renderChecks() {
const epElems = document.querySelectorAll('.load-epinfo');
const tasks = [];
for (const elem of epElems) {
const url = elem.href;
const id = generateCacheKey(url);
const task = async () => {
let hasComments;
if (cache[id]!== undefined) {
hasComments = cache[id];
} else if (elem.classList.contains('epBtnWatched')) {
try {
const dom = await retryAsyncOperation(() => getDOM(url));
hasComments = getRepliesFromDOM(dom).length > 0;
cache[id] = hasComments;
} catch (error) {
console.error(`Failed to fetch DOM for ${url}:`, error);
return;
}
} else {
return;
}
elem.classList.add(hasComments ? 'commented' : 'uncommented');
};
tasks.push(task);
}
await limitConcurrency(tasks, 5);
}
})();