Fullchan X

8chan features script

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

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