Fullchan X

8chan features script

目前为 2025-04-24 提交的版本,查看 最新版本

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