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