Fullchan X

8chan features script

Versione datata 21/04/2025. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name        Fullchan X
// @namespace   Violentmonkey Scripts
// @match       https://8chan.moe/*/res/*
// @match       https://8chan.se/*/res/*
// @match       https://8chan.moe/*/catalog*
// @match       https://8chan.se/*/catalog*
// @run-at      document-end
// @grant       none
// @version     1.10.0
// @author      vfyxe
// @description 8chan features script
// ==/UserScript==

class fullChanX extends HTMLElement {
  constructor() {
    super();
    this.settingsEl = document.querySelector('fullchan-x-settings');
    this.settings = this.settingsEl.settings;
    this.isThread = !!document.querySelector('.opCell');
    this.isDisclaimer = window.location.href.includes('disclaimer');
    Object.keys(this.settings).forEach(key => {
      this[key] = this.settings[key]?.value;
    });
  }

  init() {
    this.settingsButton = this.querySelector('#fcx-settings-btn');
    this.settingsButton.addEventListener('click', () => this.settingsEl.toggle());
    this.handleBoardLinks();
    if (!this.isThread) return;

    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.gallery = document.querySelector('fullchan-x-gallery');
    this.galleryButton = this.querySelector('#fcx-gallery-btn');

    this.updateYous();
    this.observers();

    if (this.enableFileExtentions) this.handleTruncatedFilenames();
  }

  styleUI () {
    this.style.setProperty('--top', this.uiTopPosition);
    this.style.setProperty('--right', this.uiRightPosition);
    this.classList.toggle('fcx-in-nav', this.moveToNave)
    this.classList.toggle('fcx--dim', this.uiDimWhenInactive && !this.moveToNave);
    this.classList.toggle('page-thread', this.isThread);
    const style = document.createElement('style');

    if (this.hideDefaultBoards !== '') {
      style.textContent += '#navTopBoardsSpan{display:block!important;}'
    }
    document.body.appendChild(style);
  }

  handleBoardLinks () {
    const navBoards = document.querySelector('#navTopBoardsSpan');
    const customBoardLinks = this.customBoardLinks?.toLowerCase().replace(/\s/g,'').split(',');
    let hideDefaultBoards = this.hideDefaultBoards?.toLowerCase().replace(/\s/g,'') || '';
    const urlCatalog = this.catalogBoardLinks ? '/catalog.html' : '';


    if (hideDefaultBoards === 'all') {
      navBoards.classList.add('hidden');
    } else {
      const waitForNavBoards = setInterval(() => {
        const navBoards = document.querySelector('#navTopBoardsSpan');
        if (!navBoards || !navBoards.querySelector('a')) return;

        clearInterval(waitForNavBoards);

        hideDefaultBoards = hideDefaultBoards.split(',');
        const defaultLinks = [...navBoards.querySelectorAll('a')];
        defaultLinks.forEach(link => {
          link.href += urlCatalog;
          const linkText = link.textContent;
          const shouldHide = hideDefaultBoards.includes(linkText) || customBoardLinks.includes(linkText);
          link.classList.toggle('hidden', shouldHide);
        });
      }, 50);
    }

    if (this.customBoardLinks.length > 0) {
      const customNav = document.createElement('span');
      customNav.classList = 'nav-boards nav-boards--custom';
      customNav.innerHTML = '<span>[</span>';

      customBoardLinks.forEach((board, index) => {
        const link = document.createElement('a');
        link.href = '/' + board + urlCatalog;
        link.textContent = board;
        customNav.appendChild(link);
        if (index < customBoardLinks.length - 1) customNav.innerHTML += '<span>/</span>';
      });

      customNav.innerHTML += '<span>]</span>';
      navBoards.parentNode.insertBefore(customNav, navBoards);
    }
  }

  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.settings.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());
    this.myYousLabel.addEventListener('click', (event) => {
      if (this.myYousLabel.classList.contains('unseen')) {
        this.yousContainer.querySelector('.unseen').click();
      }
    });
  }

  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') || clicked.closest('.panelBacklinks a');
    const postMedia = clicked.closest('a[data-filemime]');
    const postId = clicked.closest('.linkQuote');
    const anonId = clicked.closest('.labelId');

    if (nestQuote) {
      if (event.target.closest('.fcx-prevent-nesting')) return;
      event.preventDefault();
      this.nestQuote(nestQuote, post);
    } else if (postMedia && isNested) {
      this.handleMediaClick(event, postMedia);
    } else if (postId && isNested) {
      this.handleIdClick(postId);
    } else if (anonId) {
      this.handleAnonIdClick(anonId, event);
    }
  }

  handleAnonIdClick (anonId, event) {
    this.anonIdPosts?.remove();
    if (anonId === this.anonId) {
      this.anonId = null;
      return;
    }

    this.anonId = anonId;
    const anonIdText = anonId.textContent.split(' ')[0];
    this.anonIdPosts = document.createElement('div');
    this.anonIdPosts.classList = 'fcx-id-posts fcx-prevent-nesting';

    const match = window.location.pathname.match(/^\/[^/]+\/res\/\d+\.html/);
    const prepend = match ? `${match[0]}#` : '';

    const selector = `.postInfo:has(.labelId[style="background-color: #${anonIdText}"]) .linkQuote`;

    const postIds = [...this.threadParent.querySelectorAll(selector)].map(link => {
      const postId = link.getAttribute('href').split('#q').pop();
      const newLink = document.createElement('a');
      newLink.className = 'quoteLink';
      newLink.href = prepend + postId;
      newLink.textContent = `>>${postId}`;
                  console.log('newLink',newLink)
      return newLink;
    });

    postIds.forEach(postId => this.anonIdPosts.appendChild(postId));
    anonId.insertAdjacentElement('afterend', this.anonIdPosts);

    this.setPostListeners(this.anonIdPosts);
  }


  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 => {
      if (!fileName.textContent.includes('.')) return;
      const strings = fileName.textContent.split('.');
      const typeStr =  `.${strings.pop()}`;
      const typeEl = document.createElement('a');
      typeEl.classList = ('file-ext originalNameLink');
      typeEl.textContent = typeStr;
      fileName.dataset.fileExt = typeStr;
      fileName.textContent = strings.join('.');
      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);

    if (this.replyTabIcon === '') return;
    const icon = this.replyTabIcon;
    document.title = hasUnseenYous
      ? document.title.startsWith(`${icon} `)
        ? document.title
        : `${icon} ${document.title}`
      : document.title.replace(new RegExp(`^${icon} `), '');
  }

  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, parentPost) {
    const parentPostMessage = parentPost.querySelector('.divMessage');
    const quoteId = quoteLink.href.split('#').pop();
    const quotePost = document.getElementById(quoteId);
    if (!quotePost) return;

    const quotePostContent = quotePost.querySelector('.innerOP') || quotePost.querySelector('.innerPost');
    if (!quotePostContent) return;

    const existing = parentPost.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`);
    if (existing) {
      existing.remove();
      return;
    }

    const isReply = !quoteLink.classList.contains('quoteLink');

    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);

    if (isReply) {
      parentPostMessage.insertBefore(wrapper, parentPostMessage.firstChild);
    } else {
      quoteLink.insertAdjacentElement('afterend', wrapper);
    }

    this.setPostListeners(wrapper);
  }

  setPostListeners(parentPost) {
    const postLinks = [
      ...parentPost.querySelectorAll('.quoteLink'),
      ...parentPost.querySelectorAll('.panelBacklinks a')
    ];

    const hoverPost = (event, link) => {
      const quoteId = link.href.split('#')[1];

      let existingPost = document.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`)
        || link.closest(`.postCell[id="${quoteId}"]`);

      if (existingPost) {
        this.markedPost = existingPost.querySelector('.innerPost') || existingPost.querySelector('.innerOP');
        this.markedPost?.classList.add('markedPost');
        return;
      }

      const quotePost = document.getElementById(quoteId);

      tooltips.removeIfExists();

      const tooltip = document.createElement('div');
      tooltip.className = 'quoteTooltip';
      document.body.appendChild(tooltip);

      const rect = link.getBoundingClientRect();
      if (!api.mobile) {
        if (rect.left > window.innerWidth / 2) {
          const right = window.innerWidth - rect.left - window.scrollX;
          tooltip.style.right = `${right}px`;
        } else {
          const left = rect.right + 10 + window.scrollX;
          tooltip.style.left = `${left}px`;
        }
      }

      tooltip.style.top = `${rect.top + window.scrollY}px`;
      tooltip.style.display = 'inline';

      tooltips.loadTooltip(tooltip, link.href, quoteId);
      tooltips.currentTooltip = tooltip;
    }

    const unHoverPost = (event, link) => {
      if (!tooltips.currentTooltip) {
        this.markedPost?.classList.remove('markedPost');
        return false;
      }

      if (tooltips.unmarkReply) {
        tooltips.currentTooltip.classList.remove('markedPost');
        Array.from(tooltips.currentTooltip.getElementsByClassName('replyUnderline'))
          .forEach((a) => a.classList.remove('replyUnderline'))
        tooltips.unmarkReply = false;
      } else {
        tooltips.currentTooltip.remove();
      }

      tooltips.currentTooltip = null;
    }

    const addHoverPost = (link => {
      link.addEventListener('mouseenter', (event) => hoverPost(event, link));
      link.addEventListener('mouseleave', (event) => unHoverPost(event, link));
    });

    postLinks.forEach(link => addHoverPost(link));
  }
};

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.sizeButtons = [...this.querySelectorAll('.gallery__scale-options .scale-option')];
    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);

      const scaleButton = clicked.closest('.scale-option');
      if (scaleButton) {
        const scale = parseFloat(getComputedStyle(this.imageContainer).getPropertyValue('--scale')) || 1;
        const delta = scaleButton.id === 'fcxg-smaller' ? -0.1 : 0.1;
        const newScale = Math.max(0.1, scale + delta);
        this.imageContainer.style.setProperty('--scale', newScale.toFixed(2));
      }

      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);



class fullChanXSettings extends HTMLElement {
  constructor() {
    super();
    this.settingsKey = 'fullchan-x-settings';
    this.inputs = [];
    this.settings = {};
    this.settingsTemplate = {
      moveToNave: {
        info: 'Move Fullchan-X controls into the navbar.',
        type: 'checkbox',
        value: false
      },
      enableNestedQuotes: {
        info: 'Nest posts when clicking backlinks.',
        type: 'checkbox',
        value: true
      },
      enableFileExtentions: {
        info: 'Always show filetype on shortened file names.',
        type: 'checkbox',
        value: true
      },
      customBoardLinks: {
        info: 'List of custom boards in nav (seperate by comma)',
        type: 'input',
        value: 'v,a,b'
      },
      hideDefaultBoards: {
        info: 'List of boards to remove from nav (seperate by comma). Set as "all" to remove all.',
        type: 'input',
        value: 'interracial,mlp'
      },
      catalogBoardLinks: {
        info: 'Redirect nav board links to catalog pages.',
        type: 'checkbox',
        value: true
      },
      uiTopPosition: {
        info: 'Position from top of screen e.g. 100px',
        type: 'input',
        value: '50px'
      },
      uiRightPosition: {
        info: 'Position from right of screen e.g. 100px',
        type: 'input',
        value: '25px'
      },
      uiDimWhenInactive: {
        info: 'Dim UI when not hovering with mouse.',
        type: 'checkbox',
        value: true
      },
      replyTabIcon: {
        info: 'Set the icon/text added to tab title when you get a new (You).',
        type: 'input',
        value: '❗'
      },
    };
  }

  init() {
    this.settingsContainer = this.querySelector('.fcx-settings__settings');
    this.getSavedSettings();
    this.buildSettingsOptions();
    this.listeners();
    this.querySelector('.fcx-settings__close').addEventListener('click', () => this.close());
  }

  setSavedSettings (updated) {
    localStorage.setItem(this.settingsKey, JSON.stringify(this.settings));
    if (updated) this.classList.add('fcxs-updated');
  }

  getSavedSettings() {
    const saved = JSON.parse(localStorage.getItem(this.settingsKey));
    if (saved) this.settings = saved;
  }

  listeners() {
    this.inputs.forEach(input => {
      input.addEventListener('change', () => {
        const key = input.name;
        const value = input.type === 'checkbox' ? input.checked : input.value;
        this.settings[key].value = value;
        this.setSavedSettings(true);
      });
    });
  }

  buildSettingsOptions() {
    Object.entries(this.settingsTemplate).forEach(([key, config]) => {
      const wrapper = document.createElement('div');
      const infoWrapper = document.createElement('div');
      wrapper.classList.add('fcx-setting');
      infoWrapper.classList.add('fcx-setting__info');
      wrapper.appendChild(infoWrapper);

      const label = document.createElement('label');
      label.textContent = key
        .replace(/([A-Z])/g, ' $1')
        .replace(/^./, str => str.toUpperCase());
      label.setAttribute('for', key);
      infoWrapper.appendChild(label);

      if (config.info) {
        const info = document.createElement('p');
        info.textContent = config.info;
        infoWrapper.appendChild(info);
      }

      const savedValue = this.settings[key]?.value ?? config.value;

      let input;

      if (config.type === 'checkbox') {
        input = document.createElement('input');
        input.type = 'checkbox';
        input.checked = savedValue;
      } else if (config.type === 'input') {
        input = document.createElement('input');
        input.type = 'text';
        input.value = savedValue;
      } else if (config.type === 'select') {
        input = document.createElement('select');
        const options = config.options.split(',');
        options.forEach(opt => {
          const option = document.createElement('option');
          option.value = opt;
          option.textContent = opt;
          if (opt === savedValue) option.selected = true;
          input.appendChild(option);
        });
      }

      if (input) {
        input.id = key;
        input.name = key;
        wrapper.appendChild(input);
        this.inputs.push(input);
        this.settings[key] = { value: input.type === 'checkbox' ? input.checked : input.value };
      }

      this.settingsContainer.appendChild(wrapper);
    });

    this.setSavedSettings();
  }

  open() {
    this.classList.add('open');
  }

  close() {
    this.classList.remove('open');
  }

  toggle() {
    this.classList.toggle('open');
  }
}

window.customElements.define('fullchan-x-settings', fullChanXSettings);



// Create fullchan-x settings
const fcxs = document.createElement('fullchan-x-settings');
fcxs.innerHTML = `
  <div class="fcxs fcx-settings">
    <header>
      <span class="fcx-settings__title">
        Fullchan-X Settings
      </span>
      <button class="fcx-settings__close fullchan-x__option">Close</button>
    </header>

    <main>
      <div class="fcxs__updated-message">
        <p>Settings updated, refresh page to apply</p>
        <button class="fullchan-x__option" onClick="location.reload()">Reload page</button>
      </div>
      <div class="fcx-settings__settings"></div>
    </main>

    <footer>
    </footer>
  </div>
`;
document.body.appendChild(fcxs);
fcxs.init();


// Create fullchan-x gallery
const fcxg = document.createElement('fullchan-x-gallery');
fcxg.innerHTML = `
  <div class="fcxg gallery">
    <button id="fcxg-close" class="gallery__close fullchan-x__option">Close</button>
    <div class="gallery__scale-options">
      <button id="fcxg-smaller" class="scale-option fullchan-x__option">-</button>
      <button id="fcxg-larger" class="scale-option fullchan-x__option">+</button>
    </div>
    <div id="fcxg-images" class="gallery__images" style="--scale:1.0"></div>
    <div id="fcxg-main-image" class="gallery__main-image">
      <img src="" />
    </div>
  </div>
`;
document.body.appendChild(fcxg);



// Create fullchan-x element
const fcx = document.createElement('fullchan-x');
fcx.innerHTML = `
  <div class="fcx__controls">
    <button id="fcx-settings-btn" class="fullchan-x__option fcx-settings-toggle">
     <a>⚙️</a><span>Settings</span>
    </button>

    <div class="fullchan-x__option fullchan-x__sort thread-only">
      <a>☰</a>
      <select id="thread-sort">
        <option value="default">Default</option>
        <option value="replies">Replies</option>
        <option value="catbox">Catbox</option>
      </select>
    </div>

    <button id="fcx-gallery-btn" class="gallery__toggle fullchan-x__option thread-only">
      <a>🖼️</a><span>Gallery</span>
    </button>

    <div class="fcx__my-yous thread-only">
      <p class="my-yous__label fullchan-x__option"><a>💬</a><span>My (You)s</span></p>
      <div class="my-yous__yous fcx-prevent-nesting" id="my-yous"></div>
    </div>
  </div>
`;
(document.querySelector('#dynamicHeaderThread') || document.body).appendChild(fcx);
fcx.styleUI()
onload = (event) => fcx.init();

// Styles
const style = document.createElement('style');
style.innerHTML = `
  fullchan-x {
    --top: 50px;
    --right: 25px;
    background: var(--background-color);
    border: 1px solid var(--navbar-text-color);
    color: var(--link-color);
    font-size: 14px;
    z-index: 3;
  }

  /* Fullchan-X in nav styles */
  .fcx-in-nav {
    padding: 0;
    border-width: 0;
    line-height: 20px;
    margin-right: 2px;
  }

  .fcx-in-nav .fcx__controls:before,
  .fcx-in-nav .fcx__controls:after {
    color: var(--navbar-text-color);
    font-size: 85%;
  }

  .fcx-in-nav .fcx__controls:before {
    content: "]";
  }

  .fcx-in-nav .fcx__controls:after {
    content: "[";
  }

  .fcx-in-nav .fcx__controls,
  .fcx-in-nav:hover .fcx__controls:hover {
    flex-direction: row-reverse;
  }

  .fcx-in-nav .fcx__controls .fullchan-x__option {
    padding: 0!important;
    justify-content: center;
    background: none;
    line-height: 0;
    max-width: 20px;
    min-width: 20px;
    translate: 0 1px;
    border: solid var(--navbar-text-color) 1px !important;
  }

  .fcx-in-nav .fcx__controls .fullchan-x__option:hover {
    border: solid var(--subject-color) 1px !important;
  }

  .fcx-in-nav .fullchan-x__sort > a {
    margin-bottom: 1px;
  }

  .fcx-in-nav .fcx__controls > * {
    position: relative;
  }

  .fcx-in-nav .fcx__controls .fullchan-x__option > span,
  .fcx-in-nav .fcx__controls .fullchan-x__option:not(:hover) > select {
    display: none;
  }

  .fcx-in-nav .fcx__controls .fullchan-x__option > select {
    appearance: none;
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    font-size: 0;
  }

  .fcx-in-nav .fcx__controls .fullchan-x__option > select option {
    font-size: 12px;
  }

  .fcx-in-nav .my-yous__yous {
    position: absolute;
    left: 50%;
    translate: -50%;
    background: var(--background-color);
    border: 1px solid var(--navbar-text-color);
    padding: 14px;
  }

  /* Fullchan-X main styles */
  fullchan-x:not(.fcx-in-nav) {
    top: var(--top);
    right: var(--right);
    display: block;
    padding: 10px;
    position: fixed;
    display: block;
  }

  fullchan-x:not(.page-thread) .thread-only,
  fullchan-x:not(.page-catalog) .catalog-only {
    display: none!important;
  }

  fullchan-x:hover {
    z-index: 1000!important;
  }

  .navHeader:has(fullchan-x:hover) {
    z-index: 1000!important;
  }

  fullchan-x.fcx--dim:not(:hover) {
    opacity: 0.6;
  }

  .divPosts {
    flex-direction: column;
  }

  .fcx__controls {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }

  fullchan-x:not(:hover):not(:has(select:focus)) span,
  fullchan-x:not(:hover):not(:has(select:focus)) select {
    display: none;
    margin-left: 5px;
    z-index:3;
  }

  .fcx__controls span,
  .fcx__controls select {
    margin-left: 5px;
  }

  .fcx__controls select {
    cursor: pointer;
  }

  #thread-sort {
    border: none;
    background: none;
  }

  .my-yous__yous {
    display: none;
    flex-direction: column;
    padding-top: 10px;
    max-height: calc(100vh - 220px - var(--top));
    overflow: auto;
  }

  .fcx__my-yous:hover .my-yous__yous {
    display: flex;
  }

  .fullchan-x__option {
    display: flex;
    padding: 6px 8px;
    background: white;
    border: none !important;
    border-radius: 0.2rem;
    transition: all ease 150ms;
    cursor: pointer;
    margin: 0;
    text-align: left;
    min-width: 18px;
    min-height: 18px;
    align-items: center;
  }

  .fullchan-x__option,
  .fullchan-x__option select {
    font-size: 12px;
    font-weight: 400;
    color: #374369;
  }

  fullchan-x:not(:hover):not(:has(select:focus)) .fullchan-x__option {
    display: flex;
    justify-content: center;
  }

  #thread-sort {
    padding-right: 0;
  }

  #thread-sort:hover {
    display: block;
  }

  .innerPost:has(.quoteLink.you) {
    border-left: solid #dd003e 6px;
  }

  .innerPost:has(.youName) {
    border-left: solid #68b723 6px;
  }

  /* --- Nested quotes --- */
  .divMessage .nestedPost {
    display: inline-block;
    width: 100%;
    margin-bottom: 14px;
    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)!important;
    color: white;
  }

  .my-yous__yous .unseen {
    font-weight: 900;
    color: var(--link-hover-color);
  }

  /*--- Settings --- */
  .fcx-settings {
    display: block;
    position: fixed;
    top: 50vh;
    left: 50vw;
    translate: -50% -50%;
    padding: 20px 0;
    background: var(--background-color);
    border: 1px solid var(--navbar-text-color);
    color: var(--link-color);
    font-size: 14px;
    max-width: 480px;
    max-height: 80vh;
    overflow: scroll;
    z-index: 1000;
  }

  fullchan-x-settings:not(.open) {
    display: none;
  }

  .fcx-settings > * {
    padding: 0 20px;
  }

  .fcx-settings header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin: 0 0 15px;
    padding-bottom: 20px;
    border-bottom: 1px solid var(--navbar-text-color);
  }

  .fcx-settings__title {
    font-size: 24px;
    font-size: 24px;
    letter-spacing: 0.04em;
  }

  fullchan-x-settings:not(.fcxs-updated) .fcxs__updated-message {
    display: none;
  }

  .fcx-setting {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 0;
  }

  .fcx-setting__info {
    max-width: 60%;
  }

  .fcx-setting input[type="text"],
  .fcx-setting select {
    padding: 4px 6px;
    min-width: 35%;
  }

  .fcx-setting label {
    font-weight: 600;
  }

  .fcx-setting p {
    margin: 6px 0 0;
    font-size: 12px;
  }

  .fcx-setting + .fcx-setting {
    border-top: 1px solid var(--navbar-text-color);
  }

  .fcxs__updated-message {
    margin: 10px 0;
    text-align: center;
  }

  .fcxs__updated-message p {
    font-size: 14px;
    color: var(--error);
  }

  .fcxs__updated-message button {
    margin: 14px auto 0;
  }

  /* --- Gallery --- */
  .fct-gallery-open,
  body.fct-gallery-open,
  body.fct-gallery-open #mainPanel {
    overflow: hidden!important;
  }

  body.fct-gallery-open fullchan-x:not(.fcx-in-nav),
  body.fct-gallery-open #quick-reply {
    display: none!important;
  }

  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 {
    --scale: 1.0;
    display: flex;
    width: 100%;
    height: 100%;
    justify-content: center;
    align-content: flex-start;
    gap: 4px 8px;
    flex-wrap: wrap;
  }

  fullchan-x-gallery .imgLink {
    float: unset;
    display: block;
    zoom: var(--scale);
  }

  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 {
    border: solid 1px var(--background-color)!important;
    position: fixed;
    top: 60px;
    right: 35px;
    padding: 6px 14px;
    min-height: 30px;
    z-index: 10;
  }

  .fcxg .gallery__scale-options {
    position: fixed;
    bottom: 30px;
    right: 35px;
    display: flex;
    gap: 14px;
    z-index: 10;
  }

  .fcxg .gallery__scale-options .fullchan-x__option {
    border: solid 1px var(--background-color)!important;
    width: 35px;
    height: 35px;
    font-size: 18px;
    display: flex;
    justify-content: center;
  }

  .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 --*/
  .originalNameLink[data-file-ext] {
    display: inline-block;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    max-width: 65px;
  }

  .originalNameLink[data-file-ext]:hover {
    max-width: unset;
    white-space: normal;
    display: inline;
  }

  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;
  }

  /*-- Nav Board Links --*/
  .nav-boards--custom {
    display: flex;
    gap: 3px;
  }

  #navTopBoardsSpan.hidden ~ #navBoardsSpan,
  #navTopBoardsSpan.hidden ~ .nav-fade,
  #navTopBoardsSpan a.hidden + span {
    display: none;
  }

  /*-- Anon Unique ID posts --*/
  .postInfo .spanId {
    position: relative;
  }

  .fcx-id-posts {
    position: absolute;
    top: 0;
    left: 20px;
    translate: 0 calc(-100% - 5px);
    display: flex;
    flex-direction: column;
    padding: 10px;
    background: var(--background-color);
    border: 1px solid var(--navbar-text-color);
    width: max-content;
    max-width: 500px;
    max-height: 500px;
    overflow: auto;
    z-index: 1000;
  }

  .fcx-id-posts .nestedPost {
    pointer-events: none;
    width: auto;
  }
`;

document.head.appendChild(style);


// Asuka and Eris (fantasy Asuka) are best girls