Fullchan X

8chan features script

As of 2025-04-24. See the latest version.

  1. // ==UserScript==
  2. // @name Fullchan X
  3. // @namespace Violentmonkey Scripts
  4. // @match *://8chan.moe/*
  5. // @match *://8chan.se/*
  6. // @match *://8chan.cc/*
  7. // @match *://8chan.cc/*
  8. // @run-at document-end
  9. // @grant none
  10. // @version 1.13.1
  11. // @author vfyxe
  12. // @description 8chan features script
  13. // ==/UserScript==
  14.  
  15.  
  16. class fullChanX extends HTMLElement {
  17. constructor() {
  18. super();
  19. }
  20.  
  21. init() {
  22. this.settingsEl = document.querySelector('fullchan-x-settings');
  23. this.settingsAll = this.settingsEl.settings;
  24. this.settings = this.settingsAll.main;
  25.  
  26. this.settingsThreadBanisher = this.settingsAll.threadBanisher;
  27. this.settingsMascot = this.settingsAll.mascot;
  28. this.isThread = !!document.querySelector('.opCell');
  29. this.isDisclaimer = window.location.href.includes('disclaimer');
  30. Object.keys(this.settings).forEach(key => {
  31. this[key] = this.settings[key]?.value;
  32. });
  33.  
  34. this.settingsButton = this.querySelector('#fcx-settings-btn');
  35. this.settingsButton.addEventListener('click', () => this.settingsEl.toggle());
  36. this.handleBoardLinks();
  37. if (!this.isThread) {
  38. if (this.settingsThreadBanisher.enableThreadBanisher.value) this.banishThreads(this.settingsThreadBanisher);
  39. return;
  40. }
  41. this.quickReply = document.querySelector('#quick-reply');
  42. this.qrbody = document.querySelector('#qrbody');
  43. this.threadParent = document.querySelector('#divThreads');
  44. this.threadId = this.threadParent.querySelector('.opCell').id;
  45. this.thread = this.threadParent.querySelector('.divPosts');
  46. this.posts = [...this.thread.querySelectorAll('.postCell')];
  47. this.postOrder = 'default';
  48. this.postOrderSelect = this.querySelector('#thread-sort');
  49. this.myYousLabel = this.querySelector('.my-yous__label');
  50. this.yousContainer = this.querySelector('#my-yous');
  51.  
  52. this.gallery = document.querySelector('fullchan-x-gallery');
  53. this.galleryButton = this.querySelector('#fcx-gallery-btn');
  54.  
  55. this.updateYous();
  56. this.observers();
  57.  
  58. if (this.enableFileExtensions) this.handleTruncatedFilenames();
  59. if (this.settingsMascot.enableMascot.value) this.showMascot(this.settingsMascot);
  60.  
  61. this.styleUI();
  62. }
  63.  
  64. styleUI () {
  65. this.style.setProperty('--top', this.uiTopPosition);
  66. this.style.setProperty('--right', this.uiRightPosition);
  67. this.classList.toggle('fcx-in-nav', this.moveToNav)
  68. this.classList.toggle('fcx--dim', this.uiDimWhenInactive && !this.moveToNave);
  69. this.classList.toggle('page-thread', this.isThread);
  70. const style = document.createElement('style');
  71.  
  72. if (this.hideDefaultBoards !== '' && this.hideDefaultBoards.toLowerCase() !== 'all') {
  73. style.textContent += '#navTopBoardsSpan{display:block!important;}'
  74. }
  75. document.body.appendChild(style);
  76. }
  77.  
  78. checkRegexList(string, regexList) {
  79. const regexObjects = regexList.map(r => {
  80. const match = r.match(/^\/(.*)\/([gimsuy]*)$/);
  81. return match ? new RegExp(match[1], match[2]) : null;
  82. }).filter(Boolean);
  83.  
  84. return regexObjects.some(regex => regex.test(string));
  85. }
  86.  
  87. banishThreads(banisher) {
  88. this.threadsContainer = document.querySelector('#divThreads');
  89. if (!this.threadsContainer) return;
  90. this.threadsContainer.classList.add('fcx-threads');
  91.  
  92. const currentBoard = document.querySelector('#labelBoard')?.textContent.replace(/\//g,'');
  93. const boards = banisher.boards.value?.split(',') || [''];
  94. if (!boards.includes(currentBoard)) return;
  95.  
  96. const minCharacters = banisher.minimumCharacters.value || 0;
  97. const banishTerms = banisher.banishTerms.value?.split('\n') || [];
  98. const banishAnchored = banisher.banishAnchored.value;
  99. const wlCyclical = banisher.whitelistCyclical.value;
  100. const wlReplyCount = parseInt(banisher.whitelistReplyCount.value);
  101.  
  102. const banishSorter = (thread) => {
  103. if (thread.querySelector('.pinIndicator') || thread.classList.contains('fcx-sorted')) return;
  104. let shouldBanish = false;
  105.  
  106. const isAnchored = thread.querySelector('.bumpLockIndicator');
  107. const isCyclical = thread.querySelector('.cyclicIndicator');
  108. const replyCount = parseInt(thread.querySelector('.labelReplies')?.textContent?.trim()) || 0;
  109. const threadSubject = thread.querySelector('.labelSubject')?.textContent?.trim() || '';
  110. const threadMessage = thread.querySelector('.divMessage')?.textContent?.trim() || '';
  111. const threadContent = threadSubject + ' ' + threadMessage;
  112.  
  113. const hasMinChars = threadMessage.length > minCharacters;
  114. const hasWlReplyCount = replyCount > wlReplyCount;
  115.  
  116. if (!hasMinChars) shouldBanish = true;
  117. if (isAnchored && banishAnchored) shouldBanish = true;
  118. if (isCyclical && wlCyclical) shouldBanish = false;
  119. if (hasWlReplyCount) shouldBanish = false;
  120.  
  121. // run heavy regex process only if needed
  122. if (!shouldBanish && this.checkRegexList(threadContent, banishTerms)) shouldBanish = true;
  123. if (shouldBanish) thread.classList.add('shit-thread');
  124. thread.classList.add('fcx-sorted');
  125. };
  126.  
  127. const banishThreads = () => {
  128. this.threads = this.threadsContainer.querySelectorAll('.catalogCell');
  129. this.threads.forEach(thread => banishSorter(thread));
  130. };
  131. banishThreads();
  132.  
  133. const observer = new MutationObserver((mutationsList) => {
  134. for (const mutation of mutationsList) {
  135. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  136. banishThreads();
  137. break;
  138. }
  139. }
  140. });
  141.  
  142. observer.observe(this.threadsContainer, { childList: true });
  143. }
  144.  
  145. handleBoardLinks () {
  146. const navBoards = document.querySelector('#navTopBoardsSpan');
  147. const customBoardLinks = this.customBoardLinks?.toLowerCase().replace(/\s/g,'').split(',');
  148. let hideDefaultBoards = this.hideDefaultBoards?.toLowerCase().replace(/\s/g,'') || '';
  149. const urlCatalog = this.catalogBoardLinks ? '/catalog.html' : '';
  150.  
  151. if (hideDefaultBoards === 'all') {
  152. document.body.classList.add('hide-navboard');
  153. } else {
  154. const waitForNavBoards = setInterval(() => {
  155. const navBoards = document.querySelector('#navTopBoardsSpan');
  156. if (!navBoards || !navBoards.querySelector('a')) return;
  157.  
  158. clearInterval(waitForNavBoards);
  159.  
  160. hideDefaultBoards = hideDefaultBoards.split(',');
  161. const defaultLinks = [...navBoards.querySelectorAll('a')];
  162. defaultLinks.forEach(link => {
  163. link.href += urlCatalog;
  164. const linkText = link.textContent;
  165. const shouldHide = hideDefaultBoards.includes(linkText) || customBoardLinks.includes(linkText);
  166. link.classList.toggle('hidden', shouldHide);
  167. });
  168. }, 50);
  169. }
  170.  
  171. if (this.customBoardLinks.length > 0) {
  172. const customNav = document.createElement('span');
  173. customNav.classList = 'nav-boards nav-boards--custom';
  174. customNav.innerHTML = '<span>[</span>';
  175.  
  176. customBoardLinks.forEach((board, index) => {
  177. const link = document.createElement('a');
  178. link.href = '/' + board + urlCatalog;
  179. link.textContent = board;
  180. customNav.appendChild(link);
  181. if (index < customBoardLinks.length - 1) customNav.innerHTML += '<span>/</span>';
  182. });
  183.  
  184. customNav.innerHTML += '<span>]</span>';
  185. navBoards?.parentNode.insertBefore(customNav, navBoards);
  186. }
  187. }
  188.  
  189. observers () {
  190. this.postOrderSelect.addEventListener('change', (event) => {
  191. this.postOrder = event.target.value;
  192. this.assignPostOrder();
  193. });
  194.  
  195. const observerCallback = (mutationsList, observer) => {
  196. for (const mutation of mutationsList) {
  197. if (mutation.type === 'childList') {
  198. this.posts = [...this.thread.querySelectorAll('.postCell')];
  199. if (this.postOrder !== 'default') this.assignPostOrder();
  200. this.updateYous();
  201. this.gallery.updateGalleryImages();
  202. if (this.settings.enableFileExtensions) this.handleTruncatedFilenames();
  203. }
  204. }
  205. };
  206.  
  207. const threadObserver = new MutationObserver(observerCallback);
  208. threadObserver.observe(this.thread, { childList: true, subtree: false });
  209.  
  210. if (this.enableNestedQuotes) {
  211. this.threadParent.addEventListener('click', event => {
  212. this.handleClick(event);
  213. });
  214. }
  215.  
  216. this.galleryButton.addEventListener('click', () => this.gallery.open());
  217. this.myYousLabel.addEventListener('click', (event) => {
  218. if (this.myYousLabel.classList.contains('unseen')) {
  219. this.yousContainer.querySelector('.unseen').click();
  220. }
  221. });
  222. }
  223.  
  224. handleClick (event) {
  225. const clicked = event.target;
  226.  
  227. const innerOP = clicked.closest('.innerOP');
  228. const post = innerOP || clicked.closest('.innerPost');
  229. if (!post) return;
  230.  
  231. const isNested = !!post.closest('.innerNested');
  232. const nestQuote = clicked.closest('.quoteLink') || clicked.closest('.panelBacklinks a');
  233. const postMedia = clicked.closest('a[data-filemime]');
  234. const postId = clicked.closest('.linkQuote');
  235. const anonId = clicked.closest('.labelId');
  236.  
  237. if (nestQuote) {
  238. if (event.target.closest('.fcx-prevent-nesting')) return;
  239. event.preventDefault();
  240. if (innerOP && nestQuote.textContent.includes('OP')) return;
  241. this.nestQuote(nestQuote, post, innerOP);
  242. } else if (postMedia && isNested) {
  243. this.handleMediaClick(event, postMedia);
  244. } else if (postId && isNested) {
  245. this.handleIdClick(postId);
  246. } else if (anonId) {
  247. this.handleAnonIdClick(anonId, event);
  248. }
  249. }
  250.  
  251. handleAnonIdClick (anonId, event) {
  252. this.anonIdPosts?.remove();
  253. if (anonId === this.anonId) {
  254. this.anonId = null;
  255. return;
  256. }
  257.  
  258. this.anonId = anonId;
  259. const anonIdText = anonId.textContent.split(' ')[0];
  260. this.anonIdPosts = document.createElement('div');
  261. this.anonIdPosts.classList = 'fcx-id-posts fcx-prevent-nesting';
  262.  
  263. const match = window.location.pathname.match(/^\/[^/]+\/res\/\d+\.html/);
  264. const prepend = match ? `${match[0]}#` : '';
  265.  
  266. const selector = `.postInfo:has(.labelId[style="background-color: #${anonIdText}"]) .linkQuote`;
  267.  
  268. const postIds = [...this.threadParent.querySelectorAll(selector)].map(link => {
  269. const postId = link.getAttribute('href').split('#q').pop();
  270. const newLink = document.createElement('a');
  271. newLink.className = 'quoteLink';
  272. newLink.href = prepend + postId;
  273. newLink.textContent = `>>${postId}`;
  274. return newLink;
  275. });
  276.  
  277. postIds.forEach(postId => this.anonIdPosts.appendChild(postId));
  278. anonId.insertAdjacentElement('afterend', this.anonIdPosts);
  279.  
  280. this.setPostListeners(this.anonIdPosts);
  281. }
  282.  
  283.  
  284. handleMediaClick (event, postMedia) {
  285. if (postMedia.dataset.filemime === "video/webm") return;
  286. event.preventDefault();
  287. const imageSrc = `${postMedia.href}`;
  288. const imageEl = postMedia.querySelector('img');
  289. if (!postMedia.dataset.thumbSrc) postMedia.dataset.thumbSrc = `${imageEl.src}`;
  290.  
  291. const isExpanding = imageEl.src !== imageSrc;
  292.  
  293. if (isExpanding) {
  294. imageEl.src = imageSrc;
  295. imageEl.classList
  296. }
  297. imageEl.src = isExpanding ? imageSrc : postMedia.dataset.thumbSrc;
  298. imageEl.classList.toggle('imgExpanded', isExpanding);
  299. }
  300.  
  301. handleIdClick (postId) {
  302. const idNumber = '>>' + postId.textContent;
  303. this.quickReply.style.display = 'block';
  304. this.qrbody.value += idNumber + '\n';
  305. }
  306.  
  307. handleTruncatedFilenames () {
  308. this.postFileNames = [...this.threadParent.querySelectorAll('.originalNameLink[download]:not([data-file-ext])')];
  309. this.postFileNames.forEach(fileName => {
  310. if (!fileName.textContent.includes('.')) return;
  311. const strings = fileName.textContent.split('.');
  312. const typeStr = `.${strings.pop()}`;
  313. const typeEl = document.createElement('a');
  314. typeEl.classList = ('file-ext originalNameLink');
  315. typeEl.textContent = typeStr;
  316. fileName.dataset.fileExt = typeStr;
  317. fileName.textContent = strings.join('.');
  318. fileName.parentNode.insertBefore(typeEl, fileName.nextSibling);
  319. });
  320. }
  321.  
  322. assignPostOrder () {
  323. const postOrderReplies = (post) => {
  324. const replyCount = post.querySelectorAll('.panelBacklinks a').length;
  325. post.style.order = 100 - replyCount;
  326. }
  327.  
  328. const postOrderCatbox = (post) => {
  329. const postContent = post.querySelector('.divMessage').textContent;
  330. const matches = postContent.match(/catbox\.moe/g);
  331. const catboxCount = matches ? matches.length : 0;
  332. post.style.order = 100 - catboxCount;
  333. }
  334.  
  335. if (this.postOrder === 'default') {
  336. this.thread.style.display = 'block';
  337. return;
  338. }
  339.  
  340. this.thread.style.display = 'flex';
  341.  
  342. if (this.postOrder === 'replies') {
  343. this.posts.forEach(post => postOrderReplies(post));
  344. } else if (this.postOrder === 'catbox') {
  345. this.posts.forEach(post => postOrderCatbox(post));
  346. }
  347. }
  348.  
  349. updateYous () {
  350. this.yous = this.posts.filter(post => post.querySelector('.quoteLink.you'));
  351. this.yousLinks = this.yous.map(you => {
  352. const youLink = document.createElement('a');
  353. youLink.textContent = '>>' + you.id;
  354. youLink.href = '#' + you.id;
  355. return youLink;
  356. })
  357.  
  358. let hasUnseenYous = false;
  359. this.setUnseenYous();
  360.  
  361. this.yousContainer.innerHTML = '';
  362. this.yousLinks.forEach(you => {
  363. const youId = you.textContent.replace('>>', '');
  364. if (!this.seenYous.includes(youId)) {
  365. you.classList.add('unseen');
  366. hasUnseenYous = true
  367. }
  368. this.yousContainer.appendChild(you)
  369. });
  370.  
  371. this.myYousLabel.classList.toggle('unseen', hasUnseenYous);
  372.  
  373. if (this.replyTabIcon === '') return;
  374. const icon = this.replyTabIcon;
  375. document.title = hasUnseenYous
  376. ? document.title.startsWith(`${icon} `)
  377. ? document.title
  378. : `${icon} ${document.title}`
  379. : document.title.replace(new RegExp(`^${icon} `), '');
  380. }
  381.  
  382. observeUnseenYou(you) {
  383. you.classList.add('observe-you');
  384.  
  385. const observer = new IntersectionObserver((entries, observer) => {
  386. entries.forEach(entry => {
  387. if (entry.isIntersecting) {
  388. const id = you.id;
  389. you.classList.remove('observe-you');
  390.  
  391. if (!this.seenYous.includes(id)) {
  392. this.seenYous.push(id);
  393. localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous));
  394. }
  395.  
  396. observer.unobserve(you);
  397. this.updateYous();
  398.  
  399. }
  400. });
  401. }, { rootMargin: '0px', threshold: 0.1 });
  402.  
  403. observer.observe(you);
  404. }
  405.  
  406. setUnseenYous() {
  407. this.seenKey = `${this.threadId}-seen-yous`;
  408. this.seenYous = JSON.parse(localStorage.getItem(this.seenKey));
  409.  
  410. if (!this.seenYous) {
  411. this.seenYous = [];
  412. localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous));
  413. }
  414.  
  415. this.unseenYous = this.yous.filter(you => !this.seenYous.includes(you.id));
  416.  
  417. this.unseenYous.forEach(you => {
  418. if (!you.classList.contains('observe-you')) {
  419. this.observeUnseenYou(you);
  420. }
  421. });
  422. }
  423.  
  424. nestQuote(quoteLink, parentPost, innerOP) {
  425. const parentPostMessage = parentPost.querySelector('.divMessage');
  426. const quoteId = quoteLink.href.split('#').pop();
  427. const quotePost = document.getElementById(quoteId);
  428. if (!quotePost) return;
  429.  
  430. if (!quoteLink.textContent.includes('OP') && quoteLink.closest(`#${CSS.escape(quoteId)}`)) return;
  431.  
  432. const quotePostContent = quotePost.querySelector('.innerOP') || quotePost.querySelector('.innerPost');
  433. if (!quotePostContent) return;
  434.  
  435. const existing = parentPost.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`);
  436. if (existing) {
  437. quoteLink.classList.remove('active');
  438. existing.remove();
  439. return;
  440. }
  441.  
  442. const isReply = !quoteLink.classList.contains('quoteLink');
  443.  
  444. const wrapper = document.createElement('div');
  445. wrapper.classList.add('nestedPost');
  446. wrapper.setAttribute('data-quote-id', quoteId);
  447.  
  448. const clone = quotePostContent.cloneNode(true);
  449. clone.style.whiteSpace = 'unset';
  450. clone.classList.add('innerNested');
  451. wrapper.appendChild(clone);
  452.  
  453. if (!isReply) {
  454. quoteLink.insertAdjacentElement('afterend', wrapper);
  455. } else {
  456. parentPostMessage.insertBefore(wrapper, parentPostMessage.firstChild);
  457. quoteLink.classList.add('active');
  458. }
  459.  
  460. this.setPostListeners(wrapper);
  461. }
  462.  
  463. setPostListeners(parentPost) {
  464. const postLinks = [
  465. ...parentPost.querySelectorAll('.quoteLink'),
  466. ...parentPost.querySelectorAll('.panelBacklinks a')
  467. ];
  468.  
  469. const hoverPost = (event, link) => {
  470. const quoteId = link.href.split('#')[1];
  471.  
  472. let existingPost = document.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`)
  473. || link.closest(`.postCell[id="${quoteId}"]`);
  474.  
  475. if (existingPost) {
  476. this.markedPost = existingPost.querySelector('.innerPost') || existingPost.querySelector('.innerOP');
  477. this.markedPost?.classList.add('markedPost');
  478. return;
  479. }
  480.  
  481. const quotePost = document.getElementById(quoteId);
  482.  
  483. tooltips.removeIfExists();
  484.  
  485. const tooltip = document.createElement('div');
  486. tooltip.className = 'quoteTooltip';
  487. document.body.appendChild(tooltip);
  488.  
  489. const rect = link.getBoundingClientRect();
  490. if (!api.mobile) {
  491. if (rect.left > window.innerWidth / 2) {
  492. const right = window.innerWidth - rect.left - window.scrollX;
  493. tooltip.style.right = `${right}px`;
  494. } else {
  495. const left = rect.right + 10 + window.scrollX;
  496. tooltip.style.left = `${left}px`;
  497. }
  498. }
  499.  
  500. tooltip.style.top = `${rect.top + window.scrollY}px`;
  501. tooltip.style.display = 'inline';
  502.  
  503. tooltips.loadTooltip(tooltip, link.href, quoteId);
  504. tooltips.currentTooltip = tooltip;
  505. }
  506.  
  507. const unHoverPost = (event, link) => {
  508. if (!tooltips.currentTooltip) {
  509. this.markedPost?.classList.remove('markedPost');
  510. return false;
  511. }
  512.  
  513. if (tooltips.unmarkReply) {
  514. tooltips.currentTooltip.classList.remove('markedPost');
  515. Array.from(tooltips.currentTooltip.getElementsByClassName('replyUnderline'))
  516. .forEach((a) => a.classList.remove('replyUnderline'))
  517. tooltips.unmarkReply = false;
  518. } else {
  519. tooltips.currentTooltip.remove();
  520. }
  521.  
  522. tooltips.currentTooltip = null;
  523. }
  524.  
  525. const addHoverPost = (link => {
  526. link.addEventListener('mouseenter', (event) => hoverPost(event, link));
  527. link.addEventListener('mouseleave', (event) => unHoverPost(event, link));
  528. });
  529.  
  530. postLinks.forEach(link => addHoverPost(link));
  531. }
  532.  
  533. showMascot(settings) {
  534. const mascot = document.createElement('img');
  535. mascot.classList.add('fcx-mascot');
  536. mascot.src = settings.image.value;
  537. mascot.style.opacity = settings.opacity.value * 0.01;
  538. mascot.style.top = settings.top.value;
  539. mascot.style.left = settings.left.value;
  540. mascot.style.right = settings.right.value;
  541. mascot.style.bottom = settings.bottom.value;
  542. mascot.style.height = settings.height.value;
  543. mascot.style.width = settings.width.value;
  544. document.body.appendChild(mascot);
  545. }
  546. };
  547.  
  548. window.customElements.define('fullchan-x', fullChanX);
  549.  
  550.  
  551. class fullChanXGallery extends HTMLElement {
  552. constructor() {
  553. super();
  554. }
  555.  
  556. init() {
  557. this.fullchanX = document.querySelector('fullchan-x');
  558. this.imageContainer = this.querySelector('.gallery__images');
  559. this.mainImageContainer = this.querySelector('.gallery__main-image');
  560. this.mainImage = this.mainImageContainer.querySelector('img');
  561. this.sizeButtons = [...this.querySelectorAll('.gallery__scale-options .scale-option')];
  562. this.closeButton = this.querySelector('.gallery__close');
  563. this.listeners();
  564. this.addGalleryImages();
  565. this.initalized = true;
  566. }
  567.  
  568. addGalleryImages () {
  569. this.thumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].map(thumb => {
  570. return thumb.cloneNode(true);
  571. });
  572.  
  573. this.thumbs.forEach(thumb => {
  574. this.imageContainer.appendChild(thumb);
  575. });
  576. }
  577.  
  578. updateGalleryImages () {
  579. if (!this.initalized) return;
  580.  
  581. const newThumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].filter(thumb => {
  582. return !this.thumbs.find(thisThumb.href === thumb.href);
  583. }).map(thumb => {
  584. return thumb.cloneNode(true);
  585. });
  586.  
  587. newThumbs.forEach(thumb => {
  588. this.thumbs.push(thumb);
  589. this.imageContainer.appendChild(thumb);
  590. });
  591. }
  592.  
  593. listeners () {
  594. this.addEventListener('click', event => {
  595. const clicked = event.target;
  596.  
  597. let imgLink = clicked.closest('.imgLink');
  598. if (imgLink?.dataset.filemime === 'video/webm') return;
  599.  
  600. if (imgLink) {
  601. event.preventDefault();
  602. this.mainImage.src = imgLink.href;
  603. }
  604.  
  605. this.mainImageContainer.classList.toggle('active', !!imgLink);
  606.  
  607. const scaleButton = clicked.closest('.scale-option');
  608. if (scaleButton) {
  609. const scale = parseFloat(getComputedStyle(this.imageContainer).getPropertyValue('--scale')) || 1;
  610. const delta = scaleButton.id === 'fcxg-smaller' ? -0.1 : 0.1;
  611. const newScale = Math.max(0.1, scale + delta);
  612. this.imageContainer.style.setProperty('--scale', newScale.toFixed(2));
  613. }
  614.  
  615. if (clicked.closest('.gallery__close')) this.close();
  616. });
  617. }
  618.  
  619. open () {
  620. if (!this.initalized) this.init();
  621. this.classList.add('open');
  622. document.body.classList.add('fct-gallery-open');
  623. }
  624.  
  625. close () {
  626. this.classList.remove('open');
  627. document.body.classList.remove('fct-gallery-open');
  628. }
  629. }
  630.  
  631. window.customElements.define('fullchan-x-gallery', fullChanXGallery);
  632.  
  633.  
  634.  
  635. class fullChanXSettings extends HTMLElement {
  636. constructor() {
  637. super();
  638. this.settingsKey = 'fullchan-x-settings';
  639. this.inputs = [];
  640. this.settings = {};
  641. this.settingsTemplate = {
  642. main: {
  643. moveToNav: {
  644. info: 'Move Fullchan-X controls into the navbar.',
  645. type: 'checkbox',
  646. value: true
  647. },
  648. enableNestedQuotes: {
  649. info: 'Nest posts when clicking backlinks.',
  650. type: 'checkbox',
  651. value: true
  652. },
  653. enableFileExtensions: {
  654. info: 'Always show filetype on shortened file names.',
  655. type: 'checkbox',
  656. value: true
  657. },
  658. customBoardLinks: {
  659. info: 'List of custom boards in nav (seperate by comma)',
  660. type: 'input',
  661. value: 'v,a,b'
  662. },
  663. hideDefaultBoards: {
  664. info: 'List of boards to remove from nav (seperate by comma). Set as "all" to remove all.',
  665. type: 'input',
  666. value: 'interracial,mlp'
  667. },
  668. catalogBoardLinks: {
  669. info: 'Redirect nav board links to catalog pages.',
  670. type: 'checkbox',
  671. value: true
  672. },
  673. uiTopPosition: {
  674. info: 'Position from top of screen e.g. 100px',
  675. type: 'input',
  676. value: '50px'
  677. },
  678. uiRightPosition: {
  679. info: 'Position from right of screen e.g. 100px',
  680. type: 'input',
  681. value: '25px'
  682. },
  683. uiDimWhenInactive: {
  684. info: 'Dim UI when not hovering with mouse.',
  685. type: 'checkbox',
  686. value: true
  687. },
  688. hideNavbar: {
  689. info: 'Hide navbar until hover.',
  690. type: 'checkbox',
  691. value: false
  692. },
  693. replyTabIcon: {
  694. info: 'Set the icon/text added to tab title when you get a new (You).',
  695. type: 'input',
  696. value: '❗'
  697. }
  698. },
  699. mascot: {
  700. enableMascot: {
  701. info: 'Enable mascot image.',
  702. type: 'checkbox',
  703. value: false
  704. },
  705. image: {
  706. info: 'Image URL (8chan image recommended).',
  707. type: 'input',
  708. value: '/.static/logo.png'
  709. },
  710. opacity: {
  711. info: 'Opacity (1 to 100)',
  712. type: 'input',
  713. inputType: 'number',
  714. value: '75'
  715. },
  716. width: {
  717. info: 'Width of image.',
  718. type: 'input',
  719. value: '300px'
  720. },
  721. height: {
  722. info: 'Height of image.',
  723. type: 'input',
  724. value: 'auto'
  725. },
  726. bottom: {
  727. info: 'Bottom position.',
  728. type: 'input',
  729. value: '0px'
  730. },
  731. right: {
  732. info: 'Right position.',
  733. type: 'input',
  734. value: '0px'
  735. },
  736. top: {
  737. info: 'Top position.',
  738. type: 'input',
  739. value: ''
  740. },
  741. left: {
  742. info: 'Left position.',
  743. type: 'input',
  744. value: ''
  745. }
  746. },
  747. threadBanisher: {
  748. enableThreadBanisher: {
  749. info: 'Banish shit threads to the bottom of the calalog.',
  750. type: 'checkbox',
  751. value: true
  752. },
  753. boards: {
  754. info: 'Banish theads on these boards (seperated by comma).',
  755. type: 'input',
  756. value: 'v,a'
  757. },
  758. minimumCharacters: {
  759. info: 'Minimum character requirements',
  760. type: 'input',
  761. inputType: 'number',
  762. value: 100
  763. },
  764. banishTerms: {
  765. info: `<p>Banish threads with these terms to the bottom of the catalog (new line per term).</p>
  766. <p>How to use regex: <a href="https://www.datacamp.com/cheat-sheet/regular-expresso" target="__blank">Regex Cheatsheet</a>.</p>
  767. <p>NOTE: word breaks (\\b) MUST be entered as double escapes (\\\\b), they will appear as (\\b) when saved.</p>
  768. `,
  769. type: 'textarea',
  770. value: '/\\bcuck\\b/i\n/\\bchud\\b/i\n/\\bblacked\\b/i\n/\\bnormie\\b/i\n/\\bincel\\b/i\n/\\btranny\\b/i\n/\\bslop\\b/i\n'
  771. },
  772. whitelistCyclical: {
  773. info: 'Whitelist cyclical threads.',
  774. type: 'checkbox',
  775. value: true
  776. },
  777. banishAnchored: {
  778. info: 'Banish anchored threads that are under minimum reply count.',
  779. type: 'checkbox',
  780. value: true
  781. },
  782. whitelistReplyCount: {
  783. info: 'Threads above this reply count (excluding those with banish terms) will be whitelisted.',
  784. type: 'input',
  785. inputType: 'number',
  786. value: 100
  787. },
  788. }
  789. };
  790. }
  791.  
  792. init() {
  793. this.fcx = document.querySelector('fullchan-x');
  794. this.settingsMain = this.querySelector('.fcxs-main');
  795. this.settingsThreadBanisher = this.querySelector('.fcxs-thread-banisher');
  796. this.settingsMascot = this.querySelector('.fcxs-mascot');
  797. this.getSavedSettings();
  798. if (this.settings.main) {
  799. this.fcx.init();
  800. this.loaded = true;
  801. };
  802. this.buildSettingsOptions('main', this.settingsMain);
  803. this.buildSettingsOptions('threadBanisher', this.settingsThreadBanisher);
  804. this.buildSettingsOptions('mascot', this.settingsMascot);
  805. this.listeners();
  806. this.querySelector('.fcx-settings__close').addEventListener('click', () => this.close());
  807.  
  808. if (!this.loaded) this.fcx.init();
  809. this.fcx.styleUI();
  810. document.body.classList.toggle('fcx-hide-navbar',this.settings.main.hideNavbar.value);
  811. }
  812.  
  813. setSavedSettings(updated) {
  814. localStorage.setItem(this.settingsKey, JSON.stringify(this.settings));
  815. if (updated) this.classList.add('fcxs-updated');
  816. }
  817.  
  818. getSavedSettings() {
  819. let saved = JSON.parse(localStorage.getItem(this.settingsKey));
  820. if (!saved) return;
  821.  
  822. // Ensure all top-level keys exist
  823. for (const key in this.settingsTemplate) {
  824. if (!saved[key]) saved[key] = {};
  825. }
  826.  
  827. this.settings = saved;
  828. }
  829.  
  830. listeners() {
  831. this.inputs.forEach(input => {
  832. input.addEventListener('change', () => {
  833. const section = input.dataset.section;
  834. const key = input.name;
  835. const value = input.type === 'checkbox' ? input.checked : input.value;
  836. this.settings[section][key].value = value;
  837. this.setSavedSettings(true);
  838. });
  839. });
  840. }
  841.  
  842. buildSettingsOptions(subSettings, parent) {
  843. if (!this.settings[subSettings]) this.settings[subSettings] = {}
  844.  
  845. Object.entries(this.settingsTemplate[subSettings]).forEach(([key, config]) => {
  846. const wrapper = document.createElement('div');
  847. const infoWrapper = document.createElement('div');
  848. wrapper.classList.add('fcx-setting');
  849. infoWrapper.classList.add('fcx-setting__info');
  850. wrapper.appendChild(infoWrapper);
  851.  
  852. const label = document.createElement('label');
  853. label.textContent = key
  854. .replace(/([A-Z])/g, ' $1')
  855. .replace(/^./, str => str.toUpperCase());
  856. label.setAttribute('for', key);
  857. infoWrapper.appendChild(label);
  858.  
  859. if (config.info) {
  860. const info = document.createElement('p');
  861. info.innerHTML = config.info;
  862. infoWrapper.appendChild(info);
  863. }
  864.  
  865. const savedValue = this.settings[subSettings][key]?.value ?? config.value;
  866. let input;
  867.  
  868. if (config.type === 'checkbox') {
  869. input = document.createElement('input');
  870. input.type = 'checkbox';
  871. input.checked = savedValue;
  872. } else if (config.type === 'textarea') {
  873. input = document.createElement('textarea');
  874. input.value = savedValue;
  875. } else if (config.type === 'input') {
  876. input = document.createElement('input');
  877. input.type = config.inputType || 'text';
  878. input.value = savedValue;
  879. } else if (config.type === 'select' && config.options) {
  880. input = document.createElement('select');
  881. const options = config.options.split(',');
  882. options.forEach(opt => {
  883. const option = document.createElement('option');
  884. option.value = opt;
  885. option.textContent = opt;
  886. if (opt === savedValue) option.selected = true;
  887. input.appendChild(option);
  888. });
  889. }
  890.  
  891. if (input) {
  892. input.id = key;
  893. input.name = key;
  894. input.dataset.section = subSettings;
  895. wrapper.appendChild(input);
  896. this.inputs.push(input);
  897. this.settings[subSettings][key] = {
  898. value: input.type === 'checkbox' ? input.checked : input.value
  899. };
  900. }
  901.  
  902. parent.appendChild(wrapper);
  903. });
  904. }
  905.  
  906. open() {
  907. this.classList.add('open');
  908. }
  909.  
  910. close() {
  911. this.classList.remove('open');
  912. }
  913.  
  914. toggle() {
  915. this.classList.toggle('open');
  916. }
  917. }
  918.  
  919. window.customElements.define('fullchan-x-settings', fullChanXSettings);
  920.  
  921.  
  922.  
  923. class ToggleButton extends HTMLElement {
  924. constructor() {
  925. super();
  926. const data = this.dataset;
  927. this.onclick = () => {
  928. const target = data.target ? document.querySelector(data.target) : this;
  929. const value = data.value || 'active';
  930. !!data.set ? target.dataset[data.set] = value : target.classList.toggle(value);
  931. }
  932. }
  933. }
  934.  
  935. window.customElements.define('toggle-button', ToggleButton);
  936.  
  937.  
  938.  
  939. // Create fullchan-x gallery
  940. const fcxg = document.createElement('fullchan-x-gallery');
  941. fcxg.innerHTML = `
  942. <div class="fcxg gallery">
  943. <button id="fcxg-close" class="gallery__close fullchan-x__option">Close</button>
  944. <div class="gallery__scale-options">
  945. <button id="fcxg-smaller" class="scale-option fullchan-x__option">-</button>
  946. <button id="fcxg-larger" class="scale-option fullchan-x__option">+</button>
  947. </div>
  948. <div id="fcxg-images" class="gallery__images" style="--scale:1.0"></div>
  949. <div id="fcxg-main-image" class="gallery__main-image">
  950. <img src="" />
  951. </div>
  952. </div>
  953. `;
  954. document.body.appendChild(fcxg);
  955.  
  956.  
  957.  
  958. // Create fullchan-x element
  959. const fcx = document.createElement('fullchan-x');
  960. fcx.innerHTML = `
  961. <div class="fcx__controls">
  962. <button id="fcx-settings-btn" class="fullchan-x__option fcx-settings-toggle">
  963. <a>⚙️</a><span>Settings</span>
  964. </button>
  965.  
  966. <div class="fullchan-x__option fullchan-x__sort thread-only">
  967. <a>☰</a>
  968. <select id="thread-sort">
  969. <option value="default">Default</option>
  970. <option value="replies">Replies</option>
  971. <option value="catbox">Catbox</option>
  972. </select>
  973. </div>
  974.  
  975. <button id="fcx-gallery-btn" class="gallery__toggle fullchan-x__option thread-only">
  976. <a>🖼️</a><span>Gallery</span>
  977. </button>
  978.  
  979. <div class="fcx__my-yous thread-only">
  980. <p class="my-yous__label fullchan-x__option"><a>💬</a><span>My (You)s</span></p>
  981. <div class="my-yous__yous fcx-prevent-nesting" id="my-yous"></div>
  982. </div>
  983. </div>
  984. `;
  985. (document.querySelector('.navHeader') || document.body).appendChild(fcx);
  986.  
  987.  
  988.  
  989. // Create fullchan-x settings
  990. const fcxs = document.createElement('fullchan-x-settings');
  991. fcxs.innerHTML = `
  992. <div class="fcx-settings fcxs" data-tab="main">
  993. <header>
  994. <div class="fcxs__heading">
  995. <span class="fcx-settings__title">
  996. <img class="fcxs_logo" src="/.static/logo/logo_blue.png" height="25px" width="auto">
  997. <span>
  998. Fullchan-X Settings
  999. </span>
  1000. </span>
  1001. <button class="fcx-settings__close fullchan-x__option">Close</button>
  1002. </div>
  1003.  
  1004. <div class="fcx-settings__tab-buttons">
  1005. <toggle-button data-target=".fcxs" data-set="tab" data-value="main">
  1006. Main
  1007. </toggle-button>
  1008. <toggle-button data-target=".fcxs" data-set="tab" data-value="catalog">
  1009. catalog
  1010. </toggle-button>
  1011. <toggle-button data-target=".fcxs" data-set="tab" data-value="mascot">
  1012. Mascot
  1013. </toggle-button>
  1014. </div>
  1015. </header>
  1016.  
  1017. <main>
  1018. <div class="fcxs__updated-message">
  1019. <p>Settings updated, refresh page to apply</p>
  1020. <button class="fullchan-x__option" onClick="location.reload()">Reload page</button>
  1021. </div>
  1022.  
  1023. <div class="fcx-settings__settings">
  1024. <div class="fcxs-main fcxs-tab"></div>
  1025. <div class="fcxs-mascot fcxs-tab"></div>
  1026. <div class="fcxs-catalog fcxs-tab">
  1027. <div class="fcxs-thread-banisher"></div>
  1028. </div>
  1029. </div>
  1030. </main>
  1031.  
  1032. <footer>
  1033. </footer>
  1034. </div>
  1035. `;
  1036. document.body.appendChild(fcxs);
  1037. fcxs.init();
  1038.  
  1039.  
  1040.  
  1041. // Styles
  1042. const style = document.createElement('style');
  1043. style.innerHTML = `
  1044. .hide-navboard #navTopBoardsSpan {
  1045. display: none!important;
  1046. }
  1047.  
  1048. fullchan-x {
  1049. --top: 50px;
  1050. --right: 25px;
  1051. background: var(--background-color);
  1052. border: 1px solid var(--navbar-text-color);
  1053. color: var(--link-color);
  1054. font-size: 14px;
  1055. z-index: 3;
  1056. }
  1057.  
  1058. toggle-button {
  1059. cursor: pointer;
  1060. }
  1061.  
  1062. /* Fullchan-X in nav styles */
  1063. .fcx-in-nav {
  1064. padding: 0;
  1065. border-width: 0;
  1066. line-height: 20px;
  1067. margin-right: 2px;
  1068. background: none;
  1069. }
  1070.  
  1071. .fcx-in-nav .fcx__controls:before,
  1072. .fcx-in-nav .fcx__controls:after {
  1073. color: var(--navbar-text-color);
  1074. font-size: 85%;
  1075. }
  1076.  
  1077. .fcx-in-nav .fcx__controls:before {
  1078. content: "]";
  1079. }
  1080.  
  1081. .fcx-in-nav .fcx__controls:after {
  1082. content: "[";
  1083. }
  1084.  
  1085. .fcx-in-nav .fcx__controls,
  1086. .fcx-in-nav:hover .fcx__controls:hover {
  1087. flex-direction: row-reverse;
  1088. }
  1089.  
  1090. .fcx-in-nav .fcx__controls .fullchan-x__option {
  1091. padding: 0!important;
  1092. justify-content: center;
  1093. background: none;
  1094. line-height: 0;
  1095. max-width: 20px;
  1096. min-width: 20px;
  1097. translate: 0 1px;
  1098. border: solid var(--navbar-text-color) 1px !important;
  1099. }
  1100.  
  1101. .fcx-in-nav .fcx__controls .fullchan-x__option:hover {
  1102. border: solid var(--subject-color) 1px !important;
  1103. }
  1104.  
  1105. .fcx-in-nav .fullchan-x__sort > a {
  1106. margin-bottom: 1px;
  1107. }
  1108.  
  1109. .fcx-in-nav .fcx__controls > * {
  1110. position: relative;
  1111. }
  1112.  
  1113. .fcx-in-nav .fcx__controls .fullchan-x__option > span,
  1114. .fcx-in-nav .fcx__controls .fullchan-x__option:not(:hover) > select {
  1115. display: none;
  1116. }
  1117.  
  1118. .fcx-in-nav .fcx__controls .fullchan-x__option > select {
  1119. appearance: none;
  1120. position: absolute;
  1121. left: 0;
  1122. top: 0;
  1123. width: 100%;
  1124. height: 100%;
  1125. font-size: 0;
  1126. }
  1127.  
  1128. .fcx-in-nav .fcx__controls .fullchan-x__option > select option {
  1129. font-size: 12px;
  1130. }
  1131.  
  1132. .fcx-in-nav .my-yous__yous {
  1133. position: absolute;
  1134. left: 50%;
  1135. translate: -50%;
  1136. background: var(--background-color);
  1137. border: 1px solid var(--navbar-text-color);
  1138. padding: 14px;
  1139. }
  1140.  
  1141. .bottom-header .fcx-in-nav .my-yous__yous {
  1142. top: 0;
  1143. translate: -50% -100%;
  1144. }
  1145.  
  1146. /* Fullchan-X main styles */
  1147. fullchan-x:not(.fcx-in-nav) {
  1148. top: var(--top);
  1149. right: var(--right);
  1150. display: block;
  1151. padding: 10px;
  1152. position: fixed;
  1153. display: block;
  1154. }
  1155.  
  1156. fullchan-x:not(.page-thread) .thread-only,
  1157. fullchan-x:not(.page-catalog) .catalog-only {
  1158. display: none!important;
  1159. }
  1160.  
  1161. fullchan-x:hover {
  1162. z-index: 1000!important;
  1163. }
  1164.  
  1165. .navHeader:has(fullchan-x:hover) {
  1166. z-index: 1000!important;
  1167. }
  1168.  
  1169. fullchan-x.fcx--dim:not(:hover) {
  1170. opacity: 0.6;
  1171. }
  1172.  
  1173. .divPosts {
  1174. flex-direction: column;
  1175. }
  1176.  
  1177. .fcx__controls {
  1178. display: flex;
  1179. flex-direction: column;
  1180. gap: 6px;
  1181. }
  1182.  
  1183. fullchan-x:not(:hover):not(:has(select:focus)) span,
  1184. fullchan-x:not(:hover):not(:has(select:focus)) select {
  1185. display: none;
  1186. margin-left: 5px;
  1187. z-index:3;
  1188. }
  1189.  
  1190. .fcx__controls span,
  1191. .fcx__controls select {
  1192. margin-left: 5px;
  1193. }
  1194.  
  1195. .fcx__controls select {
  1196. cursor: pointer;
  1197. }
  1198.  
  1199. #thread-sort {
  1200. border: none;
  1201. background: none;
  1202. }
  1203.  
  1204. .my-yous__yous {
  1205. display: none;
  1206. flex-direction: column;
  1207. padding-top: 10px;
  1208. max-height: calc(100vh - 220px - var(--top));
  1209. overflow: auto;
  1210. }
  1211.  
  1212. .fcx__my-yous:hover .my-yous__yous {
  1213. display: flex;
  1214. }
  1215.  
  1216. .fullchan-x__option {
  1217. display: flex;
  1218. padding: 6px 8px;
  1219. background: white;
  1220. border: none !important;
  1221. border-radius: 0.2rem;
  1222. transition: all ease 150ms;
  1223. cursor: pointer;
  1224. margin: 0;
  1225. text-align: left;
  1226. min-width: 18px;
  1227. min-height: 18px;
  1228. align-items: center;
  1229. }
  1230.  
  1231. .fullchan-x__option,
  1232. .fullchan-x__option select {
  1233. font-size: 12px;
  1234. font-weight: 400;
  1235. color: #374369;
  1236. }
  1237.  
  1238. fullchan-x:not(:hover):not(:has(select:focus)) .fullchan-x__option {
  1239. display: flex;
  1240. justify-content: center;
  1241. }
  1242.  
  1243. #thread-sort {
  1244. padding-right: 0;
  1245. }
  1246.  
  1247. #thread-sort:hover {
  1248. display: block;
  1249. }
  1250.  
  1251. .innerPost:has(.quoteLink.you) {
  1252. border-left: solid #dd003e 6px;
  1253. }
  1254.  
  1255. .innerPost:has(.youName) {
  1256. border-left: solid #68b723 6px;
  1257. }
  1258.  
  1259. /* --- Nested quotes --- */
  1260. .divMessage .nestedPost {
  1261. display: inline-block;
  1262. width: 100%;
  1263. margin-bottom: 14px;
  1264. white-space: normal!important;
  1265. overflow-wrap: anywhere;
  1266. margin-top: 0.5em;
  1267. border: 1px solid var(--navbar-text-color);
  1268. }
  1269.  
  1270. .nestedPost .innerPost,
  1271. .nestedPost .innerOP {
  1272. width: 100%;
  1273. }
  1274.  
  1275. .nestedPost .imgLink .imgExpanded {
  1276. width: auto!important;
  1277. height: auto!important;
  1278. }
  1279.  
  1280. .my-yous__label.unseen {
  1281. background: var(--link-hover-color)!important;
  1282. color: white;
  1283. }
  1284.  
  1285. .my-yous__yous .unseen {
  1286. font-weight: 900;
  1287. color: var(--link-hover-color);
  1288. }
  1289.  
  1290. .panelBacklinks a.active {
  1291. color: #dd003e;
  1292. }
  1293.  
  1294. /*--- Settings --- */
  1295. .fcx-settings {
  1296. display: block;
  1297. position: fixed;
  1298. top: 50vh;
  1299. left: 50vw;
  1300. translate: -50% -50%;
  1301. padding: 20px 0;
  1302. background: var(--background-color);
  1303. border: 1px solid var(--navbar-text-color);
  1304. color: var(--link-color);
  1305. font-size: 14px;
  1306. max-width: 480px;
  1307. max-height: 80vh;
  1308. overflow: scroll;
  1309. min-width: 500px;
  1310. z-index: 1000;
  1311. }
  1312.  
  1313. fullchan-x-settings:not(.open) {
  1314. display: none;
  1315. }
  1316.  
  1317. .fcxs__heading,
  1318. .fcxs-tab,
  1319. .fcxs footer {
  1320. padding: 0 20px;
  1321. }
  1322.  
  1323. .fcx-settings header {
  1324. margin: 0 0 15px;
  1325. border-bottom: 1px solid var(--navbar-text-color);
  1326. }
  1327.  
  1328. .fcxs__heading {
  1329. display: flex;
  1330. align-items: center;
  1331. justify-content: space-between;
  1332. padding-bottom: 20px;
  1333. }
  1334.  
  1335. .fcx-settings__title {
  1336. display: flex;
  1337. align-items: center;
  1338. gap: 10px;
  1339. font-size: 24px;
  1340. font-size: 24px;
  1341. letter-spacing: 0.04em;
  1342. }
  1343.  
  1344. .fcxs_logo {
  1345. .margin-top: -2px;
  1346. }
  1347.  
  1348. .fcx-settings__tab-buttons {
  1349. border-top: 1px solid var(--navbar-text-color);
  1350. display: flex;
  1351. align-items: center;
  1352. }
  1353.  
  1354. .fcx-settings__tab-buttons toggle-button {
  1355. flex: 1;
  1356. padding: 15px;
  1357. font-size: 14px;
  1358. }
  1359.  
  1360. .fcx-settings__tab-buttons toggle-button + toggle-button {
  1361. border-left: 1px solid var(--navbar-text-color);
  1362. }
  1363.  
  1364. .fcx-settings__tab-buttons toggle-button:hover {
  1365. color: var(--role-color);
  1366. }
  1367.  
  1368. fullchan-x-settings:not(.fcxs-updated) .fcxs__updated-message {
  1369. display: none;
  1370. }
  1371.  
  1372. .fcxs:not([data-tab="main"]) .fcxs-main,
  1373. .fcxs:not([data-tab="catalog"]) .fcxs-catalog,
  1374. .fcxs:not([data-tab="mascot"]) .fcxs-mascot {
  1375. display: none;
  1376. }
  1377.  
  1378. .fcxs[data-tab="main"] [data-value="main"],
  1379. .fcxs[data-tab="catalog"] [data-value="catalog"],
  1380. .fcxs[data-tab="mascot"] [data-value="mascot"] {
  1381. font-weight: 700;
  1382. }
  1383.  
  1384. .fcx-setting {
  1385. display: flex;
  1386. justify-content: space-between;
  1387. align-items: center;
  1388. padding: 12px 0;
  1389. }
  1390.  
  1391. .fcx-setting__info {
  1392. max-width: 60%;
  1393. }
  1394.  
  1395. .fcx-setting input[type="text"],
  1396. .fcx-setting input[type="number"],
  1397. .fcx-setting select,
  1398. .fcx-setting textarea {
  1399. padding: 4px 6px;
  1400. min-width: 35%;
  1401. }
  1402.  
  1403. .fcx-setting textarea {
  1404. min-height: 100px;
  1405. }
  1406.  
  1407. .fcx-setting label {
  1408. font-weight: 600;
  1409. }
  1410.  
  1411. .fcx-setting p {
  1412. margin: 6px 0 0;
  1413. font-size: 12px;
  1414. }
  1415.  
  1416. .fcx-setting + .fcx-setting {
  1417. border-top: 1px solid var(--navbar-text-color);
  1418. }
  1419.  
  1420. .fcxs__updated-message {
  1421. margin: 10px 0;
  1422. text-align: center;
  1423. }
  1424.  
  1425. .fcxs__updated-message p {
  1426. font-size: 14px;
  1427. color: var(--error);
  1428. }
  1429.  
  1430. .fcxs__updated-message button {
  1431. margin: 14px auto 0;
  1432. }
  1433.  
  1434. /* --- Gallery --- */
  1435. .fct-gallery-open,
  1436. body.fct-gallery-open,
  1437. body.fct-gallery-open #mainPanel {
  1438. overflow: hidden!important;
  1439. }
  1440.  
  1441. body.fct-gallery-open fullchan-x:not(.fcx-in-nav),
  1442. body.fct-gallery-open #quick-reply {
  1443. display: none!important;
  1444. }
  1445.  
  1446. fullchan-x-gallery {
  1447. position: fixed;
  1448. top: 0;
  1449. left: 0;
  1450. width: 100%;
  1451. background: rgba(0,0,0,0.9);
  1452. display: none;
  1453. height: 100%;
  1454. overflow: auto;
  1455. }
  1456.  
  1457. fullchan-x-gallery.open {
  1458. display: block;
  1459. }
  1460.  
  1461. fullchan-x-gallery .gallery {
  1462. padding: 50px 10px 0
  1463. }
  1464.  
  1465. fullchan-x-gallery .gallery__images {
  1466. --scale: 1.0;
  1467. display: flex;
  1468. width: 100%;
  1469. height: 100%;
  1470. justify-content: center;
  1471. align-content: flex-start;
  1472. gap: 4px 8px;
  1473. flex-wrap: wrap;
  1474. }
  1475.  
  1476. fullchan-x-gallery .imgLink {
  1477. float: unset;
  1478. display: block;
  1479. zoom: var(--scale);
  1480. }
  1481.  
  1482. fullchan-x-gallery .imgLink img {
  1483. border: solid white 1px;
  1484. }
  1485.  
  1486. fullchan-x-gallery .imgLink[data-filemime="video/webm"] img {
  1487. border: solid #68b723 4px;
  1488. }
  1489.  
  1490. fullchan-x-gallery .gallery__close {
  1491. border: solid 1px var(--background-color)!important;
  1492. position: fixed;
  1493. top: 60px;
  1494. right: 35px;
  1495. padding: 6px 14px;
  1496. min-height: 30px;
  1497. z-index: 10;
  1498. }
  1499.  
  1500. .fcxg .gallery__scale-options {
  1501. position: fixed;
  1502. bottom: 30px;
  1503. right: 35px;
  1504. display: flex;
  1505. gap: 14px;
  1506. z-index: 10;
  1507. }
  1508.  
  1509. .fcxg .gallery__scale-options .fullchan-x__option {
  1510. border: solid 1px var(--background-color)!important;
  1511. width: 35px;
  1512. height: 35px;
  1513. font-size: 18px;
  1514. display: flex;
  1515. justify-content: center;
  1516. }
  1517.  
  1518. .gallery__main-image {
  1519. display: none;
  1520. position: fixed;
  1521. top: 0;
  1522. left: 0;
  1523. width: 100%;
  1524. height: 100%;
  1525. justify-content: center;
  1526. align-content: center;
  1527. background: rgba(0,0,0,0.5);
  1528. }
  1529.  
  1530. .gallery__main-image img {
  1531. padding: 40px 10px 15px;
  1532. height: auto;
  1533. max-width: calc(100% - 20px);
  1534. object-fit: contain;
  1535. }
  1536.  
  1537. .gallery__main-image.active {
  1538. display: flex;
  1539. }
  1540.  
  1541. /*-- Truncated file extentions --*/
  1542. .originalNameLink[data-file-ext] {
  1543. display: inline-block;
  1544. overflow: hidden;
  1545. white-space: nowrap;
  1546. text-overflow: ellipsis;
  1547. max-width: 65px;
  1548. }
  1549.  
  1550. .originalNameLink[data-file-ext]:hover {
  1551. max-width: unset;
  1552. white-space: normal;
  1553. display: inline;
  1554. }
  1555.  
  1556. a[data-file-ext]:hover:after {
  1557. content: attr(data-file-ext);
  1558. }
  1559.  
  1560. a[data-file-ext] + .file-ext {
  1561. pointer-events: none;
  1562. }
  1563.  
  1564. a[data-file-ext]:hover + .file-ext {
  1565. display: none;
  1566. }
  1567.  
  1568. /*-- Nav Board Links --*/
  1569. .nav-boards--custom {
  1570. display: flex;
  1571. gap: 3px;
  1572. }
  1573.  
  1574. #navTopBoardsSpan.hidden ~ #navBoardsSpan,
  1575. #navTopBoardsSpan.hidden ~ .nav-fade,
  1576. #navTopBoardsSpan a.hidden + span {
  1577. display: none;
  1578. }
  1579.  
  1580. /*-- Anon Unique ID posts --*/
  1581. .postInfo .spanId {
  1582. position: relative;
  1583. }
  1584.  
  1585. .fcx-id-posts {
  1586. position: absolute;
  1587. top: 0;
  1588. left: 20px;
  1589. translate: 0 calc(-100% - 5px);
  1590. display: flex;
  1591. flex-direction: column;
  1592. padding: 10px;
  1593. background: var(--background-color);
  1594. border: 1px solid var(--navbar-text-color);
  1595. width: max-content;
  1596. max-width: 500px;
  1597. max-height: 500px;
  1598. overflow: auto;
  1599. z-index: 1000;
  1600. }
  1601.  
  1602. .fcx-id-posts .nestedPost {
  1603. pointer-events: none;
  1604. width: auto;
  1605. }
  1606.  
  1607. /* mascot */
  1608. .fcx-mascot {
  1609. position: fixed;
  1610. z-index: -1;
  1611. }
  1612.  
  1613. .fct-gallery-open .fcx-mascot {
  1614. display: none;
  1615. }
  1616.  
  1617. /*-- Thread sorting --*/
  1618. #divThreads.fcx-threads {
  1619. display: flex!important;
  1620. flex-wrap: wrap;
  1621. justify-content: center;
  1622. }
  1623.  
  1624. .catalogCell.shit-thread {
  1625. order: 10;
  1626. filter: sepia(0.17);
  1627. }
  1628.  
  1629. .catalogCell.shit-thread .labelPage:after {
  1630. content: " 💩";
  1631. }
  1632.  
  1633. /* Hide navbar */
  1634. .fcx-hide-navbar .navHeader {
  1635. --translateY: -100%;
  1636. translate: 0 var(--translateY);
  1637. transition: ease 300ms translate;
  1638. }
  1639.  
  1640. .bottom-header.fcx-hide-navbar .navHeader {
  1641. --translateY: 100%;
  1642. }
  1643.  
  1644. .fcx-hide-navbar .navHeader:after {
  1645. content: "";
  1646. display: block;
  1647. height: 100%;
  1648. width: 100%;
  1649. left: 0;
  1650. position: absolute;
  1651. top: 100%;
  1652. }
  1653.  
  1654. .fcx-hide-navbar .navHeader:hover {
  1655. --translateY: -0%;
  1656. }
  1657.  
  1658. .bottom-header .fcx-hide-navbar .navHeader:not(:hover) {
  1659. --translateY: 100%;
  1660. }
  1661.  
  1662. .bottom-header .fcx-hide-navbar .navHeader:after {
  1663. top: -100%;
  1664. }
  1665. `;
  1666.  
  1667. document.head.appendChild(style);
  1668.  
  1669.  
  1670. // Asuka and Eris (fantasy Asuka) are best girls