您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); } })();