您需要先安装一个扩展,例如 篡改猴、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.1 // @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.threadParent = document.querySelector('#divThreads'); this.thread = this.threadParent.querySelector('.divPosts'); this.posts = [...this.thread.querySelectorAll('.postCell')]; this.postOrder = 'default'; this.postOrderSelect = this.querySelector('#thread-sort'); 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('.nestedPost'); const nestQuote = clicked.closest('.quoteLink'); const postMedia = clicked.closest('a[data-filemime]'); if (nestQuote) { event.preventDefault(); this.nestQuote(nestQuote); } else if (postMedia && isNested) { this.handleMediaClick(event, postMedia); } } handleMediaClick (event, postMedia) { if (postMedia.dataset.filemime !== "image/jpeg") 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); } 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 () { const yous = this.posts.filter(post => post.querySelector('.quoteLink.you')); const yousLinks = yous.map(you => { const youLink = document.createElement('a'); youLink.textContent = '>>' + you.id; youLink.href = '#' + you.id; return youLink; }) this.yousContainer.innerHTML = ''; yousLinks.forEach(you => this.yousContainer.appendChild(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"; 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 !important; 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; } `; document.head.appendChild(style); // Asuka and Eris (fantasy Asuka) are best girls