您需要先安装一个扩展,例如 篡改猴、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.4
- // @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.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();
- }
- 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();
- }
- }
- };
- 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';
- }
- 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;
- }
- `;
- document.head.appendChild(style);
- // Asuka and Eris (fantasy Asuka) are best girls