您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
16/04/2025, 18:06:52
当前为
// ==UserScript== // @name Fullchan X // @namespace Violentmonkey Scripts // @match https://8chan.moe/*/res/*.html* // @match https://8chan.se/*/res/*.html* // @grant none // @version 1.5.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; this.enableFileExtentions = true; } init() { this.quickReply = document.querySelector('#quick-reply'); this.qrbody = document.querySelector('#qrbody'); this.threadParent = document.querySelector('#divThreads'); this.gallery = document.querySelector('fullchan-x-gallery'); this.galleryButton = this.querySelector('#fcx-gallery-btn'); 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(); if (this.enableFileExtentions) this.handleTruncatedFilenames(); } 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(); this.gallery.updateGalleryImages(); if (this.enableFileExtentions) this.handleTruncatedFilenames(); } } }; const threadObserver = new MutationObserver(observerCallback); threadObserver.observe(this.thread, { childList: true, subtree: false }); if (this.enableNestedQuotes) { this.thread.addEventListener('click', event => { this.handleClick(event); }); } this.galleryButton.addEventListener('click', () => this.gallery.open()); } 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'; } handleTruncatedFilenames () { this.postFileNames = [...this.threadParent.querySelectorAll('.originalNameLink[download]:not([data-file-ext])')]; this.postFileNames.forEach(fileName => { const strings = fileName.textContent.split('.'); fileName.textContent = strings[0]; fileName.dataset.fileExt = `.${strings[1]}`; const typeEl = document.createElement('a'); typeEl.textContent = `.${strings[1]}`; typeEl.classList = ('file-ext originalNameLink'); fileName.parentNode.insertBefore(typeEl, fileName.nextSibling); }); } 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); document.title = hasUnseenYous ? document.title.startsWith('🔴 ') ? document.title : `🔴 ${document.title}` : document.title.replace(/^🔴 /, ''); } 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); class fullChanXGallery extends HTMLElement { constructor() { super(); } init() { this.fullchanX = document.querySelector('fullchan-x'); this.imageContainer = this.querySelector('.gallery__images'); this.mainImageContainer = this.querySelector('.gallery__main-image'); this.mainImage = this.mainImageContainer.querySelector('img'); this.closeButton = this.querySelector('.gallery__close'); this.listeners(); this.addGalleryImages(); this.initalized = true; } addGalleryImages () { this.thumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].map(thumb => { return thumb.cloneNode(true); }); this.thumbs.forEach(thumb => { this.imageContainer.appendChild(thumb); }); } updateGalleryImages () { if (!this.initalized) return; const newThumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].filter(thumb => { return !this.thumbs.find(thisThumb.href === thumb.href); }).map(thumb => { return thumb.cloneNode(true); }); newThumbs.forEach(thumb => { this.thumbs.push(thumb); this.imageContainer.appendChild(thumb); }); } listeners () { this.addEventListener('click', event => { const clicked = event.target; let imgLink = clicked.closest('.imgLink'); if (imgLink?.dataset.filemime === 'video/webm') return; if (imgLink) { event.preventDefault(); this.mainImage.src = imgLink.href; } this.mainImageContainer.classList.toggle('active', !!imgLink); if (clicked.closest('.gallery__close')) this.close(); }); } open () { if (!this.initalized) this.init(); this.classList.add('open'); document.body.classList.add('fct-gallery-open'); } close () { this.classList.remove('open'); document.body.classList.remove('fct-gallery-open'); } } window.customElements.define('fullchan-x-gallery', fullChanXGallery); // Create fullchan-x gallery const fcxg = document.createElement('fullchan-x-gallery'); fcxg.innerHTML = ` <div class="gallery"> <button id="#fcxg-close" class="gallery__close fullchan-x__option">Close</button> <div id="#fcxg-images" class="gallery__images"></div> <div id="#fcxg-main-image" class="gallery__main-image"> <img src="" /> </div> </div> `; document.body.appendChild(fcxg); // Create fullchan-x elemnt const fcx = document.createElement('fullchan-x'); fcx.innerHTML = ` <div class="fcx__controls"> <select id="thread-sort" class="fullchan-x__option"> <option value="default">Default</option> <option value="replies">Replies</option> <option value="catbox">Catbox</option> </select> <button id="fcx-gallery-btn" class="gallery__toggle fullchan-x__option">Gallery</button> <div class="fcx__my-yous"> <p class="my-yous__label fullchan-x__option">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: 50px; right: 25px; 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: 6px; } .my-yous__yous { display: none; flex-direction: column; } .fullchan-x__option { padding: 6px 8px; background: white; border: none !important; border-radius: 0.2rem; transition: all ease 150ms; cursor: pointer; font-size: 12px; font-weight: 400; color: #374369; margin: 0; text-align: left; } #thread-sort { padding-right: 0; } .fcx__my-yous:hover .my-yous__yous { display: flex; padding-top: 10px; } .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); } // --- Gallery --- .fct-gallery-open, body.fct-gallery-open, body.fct-gallery-open #mainPanel { overflow: hidden!important; position: fixed!important; //fuck you, stop scolling cunt! } body.fct-gallery-open fullchan-x { display: none; } fullchan-x-gallery { position: fixed; top: 0; left: 0; width: 100%; background: rgba(0,0,0,0.9); display: none; height: 100%; overflow: auto; } fullchan-x-gallery.open { display: block; } fullchan-x-gallery .gallery { padding: 50px 10px 0 } fullchan-x-gallery .gallery__images { display: flex; width: 100%; height: 100%; justify-content: center; align-content: flex-start; gap: 4px 8px; flex-wrap: wrap; } fullchan-x-gallery .imgLink img { border: solid white 1px; } fullchan-x-gallery .imgLink[data-filemime="video/webm"] img { border: solid #68b723 4px; } fullchan-x-gallery .gallery__close { position: fixed; top: 60px; right: 35px; padding: 6px 14px; z-index: 10; } .gallery__main-image { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; justify-content: center; align-content: center; background: rgba(0,0,0,0.5); } .gallery__main-image img { padding: 40px 10px 15px; height: auto; max-width: calc(100% - 20px); object-fit: contain; } .gallery__main-image.active { display: flex; } // Truncated file extentions [data-file-ext] {} .originalNameLink[data-file-ext] { max-width: 65px; } a[data-file-ext]:hover:after { content: attr(data-file-ext); } a[data-file-ext] + .file-ext { pointer-events: none; } a[data-file-ext]:hover + .file-ext { display: none; } `; document.head.appendChild(style); // Asuka and Eris (fantasy Asuka) are best girls