Fullchan X

8chan features script

Nainstalovat skript?
Skript doporučený autorem

Mohlo by se vám také líbit 8chanSS.

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