您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
16/04/2025, 18:06:52
当前为
// ==UserScript== // @name Fullchan X // @namespace Violentmonkey Scripts // @match https://8chan.moe/*/res/*.html* // @grant none // @version 1.3 // @author vfyxe // @description 16/04/2025, 18:06:52 // ==/UserScript== if (!document.querySelector('.divPosts')) return; class fullChanX extends HTMLElement { constructor() { super(); this.enableNestedQuotes = true; } init() { this.quickReply = document.querySelector('#quick-reply'); this.qrbody = document.querySelector('#qrbody'); this.threadParent = document.querySelector('#divThreads'); this.threadId = this.threadParent.querySelector('.opCell').id; this.thread = this.threadParent.querySelector('.divPosts'); this.posts = [...this.thread.querySelectorAll('.postCell')]; this.postOrder = 'default'; this.postOrderSelect = this.querySelector('#thread-sort'); this.myYousLabel = this.querySelector('.my-yous__label'); this.yousContainer = this.querySelector('#my-yous'); this.updateYous(); this.observers(); } observers () { this.postOrderSelect.addEventListener('change', (event) => { this.postOrder = event.target.value; this.assignPostOrder(); }); const observerCallback = (mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { this.posts = [...this.thread.querySelectorAll('.postCell')]; if (this.postOrder !== 'default') this.assignPostOrder(); this.updateYous(); } } }; const threadObserver = new MutationObserver(observerCallback); threadObserver.observe(this.thread, { childList: true, subtree: false }); if (this.enableNestedQuotes) { this.thread.addEventListener('click', event => { this.handleClick(event); }); } } handleClick (event) { const clicked = event.target; const post = clicked.closest('.innerPost') || clicked.closest('.innerOP'); if (!post) return; const isNested = !!post.closest('.innerNested'); const nestQuote = clicked.closest('.quoteLink'); const postMedia = clicked.closest('a[data-filemime]'); const postId = clicked.closest('.linkQuote'); if (nestQuote) { event.preventDefault(); this.nestQuote(nestQuote); } else if (postMedia && isNested) { this.handleMediaClick(event, postMedia); } else if (postId && isNested) { this.handleIdClick(postId); } } handleMediaClick (event, postMedia) { if (postMedia.dataset.filemime === "video/webm") return; event.preventDefault(); const imageSrc = `${postMedia.href}`; const imageEl = postMedia.querySelector('img'); if (!postMedia.dataset.thumbSrc) postMedia.dataset.thumbSrc = `${imageEl.src}`; const isExpanding = imageEl.src !== imageSrc; if (isExpanding) { imageEl.src = imageSrc; imageEl.classList } imageEl.src = isExpanding ? imageSrc : postMedia.dataset.thumbSrc; imageEl.classList.toggle('imgExpanded', isExpanding); } handleIdClick (postId) { const idNumber = '>>' + postId.textContent; this.quickReply.style.display = 'block'; this.qrbody.value += idNumber + '\n'; } assignPostOrder () { const postOrderReplies = (post) => { const replyCount = post.querySelectorAll('.panelBacklinks a').length; post.style.order = 100 - replyCount; } const postOrderCatbox = (post) => { const postContent = post.querySelector('.divMessage').textContent; const matches = postContent.match(/catbox\.moe/g); const catboxCount = matches ? matches.length : 0; post.style.order = 100 - catboxCount; } if (this.postOrder === 'default') { this.thread.style.display = 'block'; return; } this.thread.style.display = 'flex'; if (this.postOrder === 'replies') { this.posts.forEach(post => postOrderReplies(post)); } else if (this.postOrder === 'catbox') { this.posts.forEach(post => postOrderCatbox(post)); } } updateYous () { this.yous = this.posts.filter(post => post.querySelector('.quoteLink.you')); this.yousLinks = this.yous.map(you => { const youLink = document.createElement('a'); youLink.textContent = '>>' + you.id; youLink.href = '#' + you.id; return youLink; }) let hasUnseenYous = false; this.setUnseenYous(); this.yousContainer.innerHTML = ''; this.yousLinks.forEach(you => { const youId = you.textContent.replace('>>', ''); if (!this.seenYous.includes(youId)) { you.classList.add('unseen'); hasUnseenYous = true } this.yousContainer.appendChild(you) }); this.myYousLabel.classList.toggle('unseen', hasUnseenYous); } observeUnseenYou(you) { you.classList.add('observe-you'); const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const id = you.id; you.classList.remove('observe-you'); if (!this.seenYous.includes(id)) { this.seenYous.push(id); localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous)); } observer.unobserve(you); this.updateYous(); } }); }, { rootMargin: '0px', threshold: 0.1 }); observer.observe(you); } setUnseenYous() { this.seenKey = `${this.threadId}-seen-yous`; this.seenYous = JSON.parse(localStorage.getItem(this.seenKey)); if (!this.seenYous) { this.seenYous = []; localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous)); } this.unseenYous = this.yous.filter(you => !this.seenYous.includes(you.id)); this.unseenYous.forEach(you => { if (!you.classList.contains('observe-you')) { this.observeUnseenYou(you); } }); } nestQuote(quoteLink) { const parentPostMessage = quoteLink.closest('.divMessage'); const quoteId = quoteLink.href.split('#')[1]; const quotePost = document.getElementById(quoteId); if (!quotePost) return; const quotePostContent = quotePost.querySelector('.innerOP') || quotePost.querySelector('.innerPost'); if (!quotePostContent) return; const existing = parentPostMessage.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`); if (existing) { existing.remove(); return; } const wrapper = document.createElement('div'); wrapper.classList.add('nestedPost'); wrapper.setAttribute('data-quote-id', quoteId); const clone = quotePostContent.cloneNode(true); clone.style.whiteSpace = 'unset'; clone.classList.add('innerNested'); wrapper.appendChild(clone); parentPostMessage.appendChild(wrapper); } }; window.customElements.define('fullchan-x', fullChanX); // Create fullchan-x elemnt const fcx = document.createElement('fullchan-x'); fcx.innerHTML = ` <div class="fcx__controls"> <select id="thread-sort"> <option value="default">Default</option> <option value="replies">Replies</option> <option value="catbox">Catbox</option> </select> <div class="fcx__my-yous"> <p class="my-yous__label">My (You)s</p> <div class="my-yous__yous" id="my-yous"></div> </div> </div> `; document.body.appendChild(fcx); fcx.init(); // Styles const style = document.createElement('style'); style.innerHTML = ` fullchan-x { display: block; position: fixed; top: 2.5rem; right: 2rem; padding: 10px; background: var(--contrast-color); border: 1px solid var(--navbar-text-color); color: var(--link-color); font-size: 14px; opacity: 0.5; } fullchan-x:hover { opacity: 1; } .divPosts { flex-direction: column; } .fcx__controls { display: flex; flex-direction: column; gap: 2px; } #thread-sort { padding: 0.4rem 0.6rem; background: white !important; border: none !important; border-radius: 0.2rem; transition: all ease 150ms; cursor: pointer; } .my-yous__yous { display: none; flex-direction: column; } .my-yous__label { padding: 0.4rem 0.6rem; background: white; border: none !important; border-radius: 0.2rem; transition: all ease 150ms; cursor: pointer; } .fcx__my-yous:hover .my-yous__yous { display: flex; } .innerPost:has(.quoteLink.you) { border-left: solid #dd003e 6px; } .innerPost:has(.youName) { border-left: solid #68b723 6px; } // --- Nested quotes ---- // I don't know why it needs this, weird CSS inheritance on cloned element .nestedPost {} .divMessage .nestedPost { display: block; white-space: normal!important; overflow-wrap: anywhere; margin-top: 0.5em; border: 1px solid var(--navbar-text-color); } .nestedPost .innerPost, .nestedPost .innerOP { width: 100%; } .nestedPost .imgLink .imgExpanded { width: auto!important; height: auto!important; } .my-yous__label.unseen { background: var(--link-hover-color); color: white; } .my-yous__yous .unseen { font-weight: 900; color: var(--link-hover-color); } `; document.head.appendChild(style); // Asuka and Eris (fantasy Asuka) are best girls