Fullchan X

8chan features script

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

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