Fullchan X

8chan features script

От 20.04.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==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-idle
// @grant       none
// @version     1.7.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--dim', this.uiDimWhenInactive);
    this.classList.toggle('page-thread', this.isThread);
    const style = document.createElement('style');

    console.log("this.hideDefaultBoards !== ''", this.hideDefaultBoards !== '')

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

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

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

    this.setPostListeners(wrapper);
  }

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

    // <a class="quoteLink" href="/v/res/1129326.html#1163715">&gt;&gt;1163715</a>
    // <a href="#1164807">&gt;&gt;1164807</a>

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

      console.log('quoteId',quoteId);

      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);
      console.log('quotePost',quotePost)

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



class fullChanXSettings extends HTMLElement {
  constructor() {
    super();
    this.settingsKey = 'fullchan-x-settings';
    this.inputs = [];
    this.settings = {};
    this.settingsTemplate = {
      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="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 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">
      ⚙️<span>Settings</span>
    </button>

    <div class="fullchan-x__option thread-only">
      ☰
      <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">
      🖼️<span>Gallery</span>
    </button>

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


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

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

  fullchan-x:not(:hover):not(:has(select:focus)) {
    z-index: 3;
  }

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

  #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: 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);
  }



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

  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;
    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;
    min-height: 30px;
    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 --*/
  .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;
  }

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

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

document.head.appendChild(style);


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