Guest User

Fullchan X edit

a guest
Oct 3rd, 2025
4
0
3 days
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 78.51 KB | None | 0 0
  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. // @grant GM.getValue
  9. // @grant GM.setValue
  10. // @grant GM.deleteValue
  11. // @grant GM.listValues
  12. // @run-at document-end
  13. // @version 1.30.1
  14. // @author vfyxe
  15. // @description 8chan features script
  16. // @downloadURL none
  17. // ==/UserScript==
  18.  
  19. class fullChanX extends HTMLElement {
  20. constructor() {
  21. super();
  22. }
  23.  
  24. async init() {
  25. this.settingsEl = document.querySelector('fullchan-x-settings');
  26. this.settingsAll = this.settingsEl?.settings;
  27.  
  28. if (!this.settingsAll) {
  29. const savedSettings = await GM.getValue('fullchan-x-settings');
  30. if (savedSettings) {
  31. try {
  32. this.settingsAll = JSON.parse(savedSettings);
  33. } catch (error) {
  34. console.error('Failed to parse settings from GM storage', error);
  35. this.settingsAll = {};
  36. }
  37. } else {
  38. this.settingsAll = {};
  39. }
  40. }
  41.  
  42. this.settings = this.settingsAll.main || {};
  43. this.settingsThreadBanisher = this.settingsAll.threadBanisher || {};
  44. this.settingsMascot = this.settingsAll.mascot || {};
  45.  
  46. this.isThread = !!document.querySelector('.opCell');
  47. this.isDisclaimer = window.location.href.includes('disclaimer');
  48.  
  49. Object.keys(this.settings).forEach(key => {
  50. if (typeof this.settings[key] === 'object' && this.settings[key] !== null) {
  51. this[key] = this.settings[key]?.value;
  52. } else {
  53. this[key] = this.settings[key];
  54. }
  55. });
  56.  
  57. this.settingsButton = this.querySelector('#fcx-settings-btn');
  58. this.settingsButton.addEventListener('click', () => this.settingsEl.toggle());
  59.  
  60. this.handleBoardLinks();
  61.  
  62. this.styleUI();
  63.  
  64. this.headerMenuEl = document.querySelector('fullchan-x-header-menu');
  65. if (this.headerMenuEl && this.settings.enableHeaderMenu) this.headerMenuEl.init();
  66.  
  67. if (!this.isThread) {
  68. if (this.settingsThreadBanisher.enableThreadBanisher) this.banishThreads(this.settingsThreadBanisher);
  69. return;
  70. }
  71.  
  72. this.quickReply = document.querySelector('#quick-reply');
  73. this.qrbody = document.querySelector('#qrbody');
  74. this.threadParent = document.querySelector('#divThreads');
  75. this.threadId = this.threadParent.querySelector('.opCell').id;
  76. this.thread = this.threadParent.querySelector('.divPosts');
  77. this.posts = [...this.thread.querySelectorAll('.postCell')];
  78. this.postOrder = 'default';
  79. this.postOrderSelect = this.querySelector('#thread-sort');
  80. this.myYousLabel = this.querySelector('.my-yous__label');
  81. this.yousContainer = this.querySelector('#my-yous');
  82. this.gallery = document.querySelector('fullchan-x-gallery');
  83. this.galleryButton = this.querySelector('#fcx-gallery-btn');
  84.  
  85. this.updateYous();
  86. this.observers();
  87.  
  88. if (this.enableFileExtensions) this.handleTruncatedFilenames();
  89. if (this.settingsMascot.enableMascot) this.showMascot();
  90.  
  91. if (this.settings.doNotShowLocation) {
  92. const checkbox = document.getElementById('qrcheckboxNoFlag');
  93. if (checkbox) checkbox.checked = true;
  94. checkbox.dispatchEvent(new Event('change', { bubbles: true }));
  95. }
  96. }
  97.  
  98. styleUI () {
  99. this.style.setProperty('--top', this.uiTopPosition);
  100. this.style.setProperty('--right', this.uiRightPosition);
  101. this.classList.toggle('fcx-in-nav', this.moveToNav)
  102. this.classList.toggle('fcx--dim', this.uiDimWhenInactive && !this.moveToNave);
  103. this.classList.toggle('fcx-page-thread', this.isThread);
  104. document.body.classList.toggle('fcx-replies-plus', this.enableEnhancedReplies);
  105. document.body.classList.toggle('fcx-hide-delete', this.hideDeletionBox);
  106. document.body.classList.toggle('fcx-hide-navbar', this.settings.hideNavbar);
  107. document.body.classList.toggle('fcx-icon-replies', this.settings.enableIconBacklinks);
  108. const style = document.createElement('style');
  109.  
  110. if (this.hideDefaultBoards !== '' && this.hideDefaultBoards.toLowerCase() !== 'all') {
  111. style.textContent += '#navTopBoardsSpan{display:block!important;}'
  112. }
  113. document.body.appendChild(style);
  114. }
  115.  
  116. checkRegexList(string, regexList) {
  117. const regexObjects = regexList.map(r => {
  118. const match = r.match(/^\/(.*)\/([gimsuy]*)$/);
  119. return match ? new RegExp(match[1], match[2]) : null;
  120. }).filter(Boolean);
  121.  
  122. return regexObjects.some(regex => regex.test(string));
  123. }
  124.  
  125. banishThreads(banisher) {
  126. this.threadsContainer = document.querySelector('#divThreads');
  127. if (!this.threadsContainer) return;
  128. this.threadsContainer.classList.add('fcx-threads');
  129.  
  130. const currentBoard = document.querySelector('#labelBoard')?.textContent.replace(/\//g,'');
  131. const boards = banisher.boards.value?.split(',') || [''];
  132. if (!boards.includes(currentBoard)) return;
  133.  
  134. const minCharacters = banisher.minimumCharacters.value || 0;
  135. const banishTerms = banisher.banishTerms.value?.split('\n') || [];
  136. const banishAnchored = banisher.banishAnchored.value;
  137. const wlCyclical = banisher.whitelistCyclical.value;
  138. const wlReplyCount = parseInt(banisher.whitelistReplyCount.value);
  139.  
  140. const banishSorter = (thread) => {
  141. if (thread.querySelector('.pinIndicator') || thread.classList.contains('fcx-sorted')) return;
  142. let shouldBanish = false;
  143.  
  144. const isAnchored = thread.querySelector('.bumpLockIndicator');
  145. const isCyclical = thread.querySelector('.cyclicIndicator');
  146. const replyCount = parseInt(thread.querySelector('.labelReplies')?.textContent?.trim()) || 0;
  147. const threadSubject = thread.querySelector('.labelSubject')?.textContent?.trim() || '';
  148. const threadMessage = thread.querySelector('.divMessage')?.textContent?.trim() || '';
  149. const threadContent = threadSubject + ' ' + threadMessage;
  150.  
  151. const hasMinChars = threadMessage.length > minCharacters;
  152. const hasWlReplyCount = replyCount > wlReplyCount;
  153.  
  154. if (!hasMinChars) shouldBanish = true;
  155. if (isAnchored && banishAnchored) shouldBanish = true;
  156. if (isCyclical && wlCyclical) shouldBanish = false;
  157. if (hasWlReplyCount) shouldBanish = false;
  158.  
  159. // run heavy regex process only if needed
  160. if (!shouldBanish && this.checkRegexList(threadContent, banishTerms)) shouldBanish = true;
  161. if (shouldBanish) thread.classList.add('shit-thread');
  162. thread.classList.add('fcx-sorted');
  163. };
  164.  
  165. const banishThreads = () => {
  166. this.threads = this.threadsContainer.querySelectorAll('.catalogCell');
  167. this.threads.forEach(thread => banishSorter(thread));
  168. };
  169. banishThreads();
  170.  
  171. const observer = new MutationObserver((mutationsList) => {
  172. for (const mutation of mutationsList) {
  173. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  174. banishThreads();
  175. break;
  176. }
  177. }
  178. });
  179.  
  180. observer.observe(this.threadsContainer, { childList: true });
  181. }
  182.  
  183. handleBoardLinks () {
  184. const navBoards = document.querySelector('#navTopBoardsSpan');
  185. const customBoardLinks = this.customBoardLinks?.toLowerCase().replace(/\s/g,'').split(',');
  186. console.log(customBoardLinks)
  187. let hideDefaultBoards = this.hideDefaultBoards?.toLowerCase().replace(/\s/g,'') || '';
  188. const urlCatalog = this.catalogBoardLinks ? '/catalog.html' : '';
  189.  
  190. if (hideDefaultBoards === 'all') {
  191. document.body.classList.add('fcx-hide-navboard');
  192. } else {
  193. const waitForNavBoards = setInterval(() => {
  194. const navBoards = document.querySelector('#navTopBoardsSpan');
  195. if (!navBoards || !navBoards.querySelector('a')) return;
  196.  
  197. clearInterval(waitForNavBoards);
  198.  
  199. hideDefaultBoards = hideDefaultBoards.split(',');
  200. const defaultLinks = [...navBoards.querySelectorAll('a')];
  201. defaultLinks.forEach(link => {
  202. link.href += urlCatalog;
  203. const linkText = link.textContent;
  204. const shouldHide = hideDefaultBoards.includes(linkText) || customBoardLinks.includes(linkText);
  205. link.classList.toggle('fcx-hidden', shouldHide);
  206. });
  207. }, 50);
  208.  
  209. if (this.customBoardLinks?.length > 0) {
  210. const customNav = document.createElement('span');
  211. customNav.classList = 'nav-boards nav-boards--custom';
  212. customNav.innerHTML = '<span>[</span>';
  213.  
  214. customBoardLinks.forEach((board, index) => {
  215. const link = document.createElement('a');
  216. link.href = '/' + board + urlCatalog;
  217. link.textContent = board;
  218. customNav.appendChild(link);
  219. if (index < customBoardLinks.length - 1) customNav.innerHTML += '<span>/</span>';
  220. });
  221.  
  222. customNav.innerHTML += '<span>]</span>';
  223. navBoards?.parentNode.insertBefore(customNav, navBoards);
  224. }
  225. }
  226. }
  227.  
  228. observers () {
  229. this.postOrderSelect.addEventListener('change', (event) => {
  230. this.postOrder = event.target.value;
  231. this.assignPostOrder();
  232. });
  233.  
  234.  
  235. // Thread click
  236. this.threadParent.addEventListener('click', event => this.handleClick(event));
  237.  
  238.  
  239. // Your (You)s
  240. const observerCallback = (mutationsList, observer) => {
  241. for (const mutation of mutationsList) {
  242. if (mutation.type === 'childList') {
  243. this.posts = [...this.thread.querySelectorAll('.postCell')];
  244. if (this.postOrder !== 'default') this.assignPostOrder();
  245. this.updateYous();
  246. this.gallery.updateGalleryImages();
  247. if (this.settings.enableFileExtensions) this.handleTruncatedFilenames();
  248. }
  249. }
  250. };
  251. const threadObserver = new MutationObserver(observerCallback);
  252. threadObserver.observe(this.thread, { childList: true, subtree: false });
  253.  
  254.  
  255. // Gallery
  256. this.galleryButton.addEventListener('click', () => this.gallery.open());
  257. this.myYousLabel.addEventListener('click', (event) => {
  258. if (this.myYousLabel.classList.contains('fcx-unseen')) {
  259. this.yousContainer.querySelector('.fcx-unseen').click();
  260. }
  261. });
  262.  
  263. if (!this.enableEnhancedReplies) return;
  264. const setReplyLocation = (replyPreview) => {
  265. const parent = replyPreview.parentElement;
  266. if (!parent || (!parent.classList.contains('innerPost') && !parent.classList.contains('innerOP'))) return;
  267. if (parent.querySelector('.postInfo .panelBacklinks').style.display === 'none') return;
  268. const parentMessage = parent.querySelector('.divMessage');
  269.  
  270. if (parentMessage && parentMessage.parentElement === parent) {
  271. parentMessage.insertAdjacentElement('beforebegin', replyPreview);
  272. }
  273. };
  274.  
  275. const observer = new MutationObserver(mutations => {
  276. for (const mutation of mutations) {
  277. for (const node of mutation.addedNodes) {
  278. if (node.nodeType !== 1) continue;
  279.  
  280. if (node.classList.contains('inlineQuote')) {
  281. const replyPreview = node.closest('.replyPreview');
  282. if (replyPreview) {
  283. setReplyLocation(replyPreview);
  284. }
  285. }
  286. }
  287. }
  288. });
  289.  
  290. if (this.threadParent) observer.observe(this.threadParent, {childList: true, subtree: true });
  291. }
  292.  
  293. handleClick (event) {
  294. const clicked = event.target;
  295. let replyLink = clicked.closest('.panelBacklinks a');
  296. const parentPost = clicked.closest('.innerPost, .innerOP');
  297. const closeButton = clicked.closest('.postInfo > a:first-child');
  298. const anonId = clicked.closest('.labelId');
  299. const addMascotButton = clicked.closest('.sizeLabel');
  300.  
  301. if (closeButton) this.handleReplyCloseClick(closeButton, parentPost);
  302. if (replyLink) this.handleReplyClick(replyLink, parentPost);
  303. if (anonId && this.enableIdPostList) this.handleAnonIdClick(anonId, event);
  304. if (addMascotButton) this.handleAddMascotClick(addMascotButton, event);
  305. }
  306.  
  307. handleAddMascotClick(button, event) {
  308. event.preventDefault();
  309. try {
  310. const parentEl = button.closest('.uploadDetails');
  311.  
  312. if (!parentEl) return;
  313. const linkEl = parentEl.querySelector('.originalNameLink');
  314. const imageUrl = linkEl.href;
  315. const imageName = linkEl.textContent;
  316.  
  317. this.settingsEl.addMascotFromPost(imageUrl, imageName, button);
  318. } catch (error) {
  319. console.log(error);
  320. }
  321. }
  322.  
  323. handleReplyCloseClick(closeButton, parentPost) {
  324. const replyLink = document.querySelector(`[data-close-id="${closeButton.id}"]`);
  325. if (!replyLink) return;
  326. const linkParent = replyLink.closest('.innerPost, .innerOP');
  327. this.handleReplyClick(replyLink, linkParent);
  328. }
  329.  
  330. handleReplyClick(replyLink, parentPost) {
  331. replyLink.classList.toggle('fcx-active');
  332. let replyColor = replyLink.dataset.color;
  333. const replyId = replyLink.href.split('#').pop();
  334. let replyPost = false;
  335. let labelId = false;
  336.  
  337. const randomNum = () => `${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`
  338.  
  339. if (!replyColor) {
  340. replyPost = document.querySelector(`#${CSS.escape(replyId)}`);
  341. labelId = replyPost?.querySelector('.labelId');
  342. replyColor = labelId?.textContent || randomNum();
  343. if (labelId) replyLink.dataset.hasId = true;
  344. }
  345.  
  346. const linkQuote = [...parentPost.querySelectorAll('.replyPreview .linkQuote')]
  347. .find(link => link.textContent === replyId);
  348. if (!labelId && !replyLink.dataset.hasId) {
  349. linkQuote.style = `--active-color: #${replyColor};`;
  350. linkQuote.classList.add('fcx-active-color');
  351. }
  352.  
  353. const closeId = randomNum();
  354. const closeButton = linkQuote?.closest('.innerPost').querySelector('.postInfo > a:first-child');
  355. if (closeButton) closeButton.id = closeId;
  356.  
  357. replyLink.style = `--active-color: #${replyColor};`;
  358. replyLink.dataset.color = `${replyColor}`;
  359. replyLink.dataset.closeId = closeId;
  360. }
  361.  
  362. handleAnonIdClick (anonId, event) {
  363. this.anonIdPosts?.remove();
  364. if (anonId === this.anonId) {
  365. this.anonId = null;
  366. return;
  367. }
  368.  
  369. this.anonId = anonId;
  370. const anonIdText = anonId.textContent.split(' ')[0];
  371. this.anonIdPosts = document.createElement('div');
  372. this.anonIdPosts.classList = 'fcx-id-posts fcx-prevent-nesting';
  373.  
  374. const match = window.location.pathname.match(/^\/[^/]+\/res\/\d+\.html/);
  375. const prepend = match ? `${match[0]}#` : '';
  376.  
  377. const selector = `.postInfo:has(.labelId[style="background-color: #${anonIdText}"]) .linkQuote`;
  378.  
  379. const postIds = [...this.threadParent.querySelectorAll(selector)].map(link => {
  380. const postId = link.getAttribute('href').split('#q').pop();
  381. const newLink = document.createElement('a');
  382. newLink.className = 'quoteLink';
  383. newLink.href = prepend + postId;
  384. newLink.textContent = `>>${postId}`;
  385. return newLink;
  386. });
  387.  
  388. postIds.forEach(postId => this.anonIdPosts.appendChild(postId));
  389. anonId.insertAdjacentElement('afterend', this.anonIdPosts);
  390.  
  391. this.setPostListeners(this.anonIdPosts);
  392. }
  393.  
  394. setPostListeners(parentPost) {
  395. const postLinks = [...parentPost.querySelectorAll('.quoteLink')];
  396.  
  397. const hoverPost = (event, link) => {
  398. const quoteId = link.href.split('#')[1];
  399.  
  400. let existingPost = document.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`)
  401. || link.closest(`.postCell[id="${quoteId}"]`);
  402.  
  403. if (existingPost) {
  404. this.markedPost = existingPost.querySelector('.innerPost') || existingPost.querySelector('.innerOP');
  405. this.markedPost?.classList.add('markedPost');
  406. return;
  407. }
  408.  
  409. const quotePost = document.getElementById(quoteId);
  410.  
  411. tooltips.removeIfExists();
  412.  
  413. const tooltip = document.createElement('div');
  414. tooltip.className = 'quoteTooltip';
  415. document.body.appendChild(tooltip);
  416.  
  417. const rect = link.getBoundingClientRect();
  418. if (!api.mobile) {
  419. if (rect.left > window.innerWidth / 2) {
  420. const right = window.innerWidth - rect.left - window.scrollX;
  421. tooltip.style.right = `${right}px`;
  422. } else {
  423. const left = rect.right + 10 + window.scrollX;
  424. tooltip.style.left = `${left}px`;
  425. }
  426. }
  427.  
  428. tooltip.style.top = `${rect.top + window.scrollY}px`;
  429. tooltip.style.display = 'inline';
  430.  
  431. tooltips.loadTooltip(tooltip, link.href, quoteId);
  432. tooltips.currentTooltip = tooltip;
  433. }
  434.  
  435. const unHoverPost = (event, link) => {
  436. if (!tooltips.currentTooltip) {
  437. this.markedPost?.classList.remove('markedPost');
  438. return false;
  439. }
  440.  
  441. if (tooltips.unmarkReply) {
  442. tooltips.currentTooltip.classList.remove('markedPost');
  443. Array.from(tooltips.currentTooltip.getElementsByClassName('replyUnderline'))
  444. .forEach((a) => a.classList.remove('replyUnderline'))
  445. tooltips.unmarkReply = false;
  446. } else {
  447. tooltips.currentTooltip.remove();
  448. }
  449.  
  450. tooltips.currentTooltip = null;
  451. }
  452.  
  453. const addHoverPost = (link => {
  454. link.addEventListener('mouseenter', (event) => hoverPost(event, link));
  455. link.addEventListener('mouseleave', (event) => unHoverPost(event, link));
  456. });
  457.  
  458. postLinks.forEach(link => addHoverPost(link));
  459. }
  460.  
  461. handleTruncatedFilenames () {
  462. this.postFileNames = [...this.threadParent.querySelectorAll('.originalNameLink[download]:not([data-file-ext])')];
  463. this.postFileNames.forEach(fileName => {
  464. if (!fileName.textContent.includes('.')) return;
  465. const strings = fileName.textContent.split('.');
  466. const typeStr = `.${strings.pop()}`;
  467. const typeEl = document.createElement('a');
  468. typeEl.classList = ('file-ext originalNameLink');
  469. typeEl.textContent = typeStr;
  470. fileName.dataset.fileExt = typeStr;
  471. fileName.textContent = strings.join('.');
  472. fileName.parentNode.insertBefore(typeEl, fileName.nextSibling);
  473. });
  474. }
  475.  
  476. assignPostOrder () {
  477. const postOrderReplies = (post) => {
  478. const replyCount = post.querySelectorAll('.panelBacklinks a').length;
  479. post.style.order = 100 - replyCount;
  480. }
  481.  
  482. const postOrderCatbox = (post) => {
  483. const postContent = post.querySelector('.divMessage').textContent;
  484. const matches = postContent.match(/catbox\.moe/g);
  485. const catboxCount = matches ? matches.length : 0;
  486. post.style.order = 100 - catboxCount;
  487. }
  488.  
  489. if (this.postOrder === 'default') {
  490. this.thread.style.display = 'block';
  491. return;
  492. }
  493.  
  494. this.thread.style.display = 'flex';
  495.  
  496. if (this.postOrder === 'replies') {
  497. this.posts.forEach(post => postOrderReplies(post));
  498. } else if (this.postOrder === 'catbox') {
  499. this.posts.forEach(post => postOrderCatbox(post));
  500. }
  501. }
  502.  
  503. updateYous () {
  504. this.yous = this.posts.filter(post => post.querySelector('.quoteLink.you'));
  505. this.yousLinks = this.yous.map(you => {
  506. const youLink = document.createElement('a');
  507. youLink.textContent = '>>' + you.id;
  508. youLink.href = '#' + you.id;
  509. return youLink;
  510. })
  511.  
  512. let hasUnseenYous = false;
  513. this.setUnseenYous();
  514.  
  515. this.yousContainer.innerHTML = '';
  516. this.yousLinks.forEach(you => {
  517. const youId = you.textContent.replace('>>', '');
  518. if (!this.seenYous.includes(youId)) {
  519. you.classList.add('fcx-unseen');
  520. hasUnseenYous = true
  521. }
  522. this.yousContainer.appendChild(you)
  523. });
  524.  
  525. this.myYousLabel.classList.toggle('fcx-unseen', hasUnseenYous);
  526.  
  527. if (this.replyTabIcon === '') return;
  528. const icon = this.replyTabIcon;
  529. document.title = hasUnseenYous
  530. ? document.title.startsWith(`${icon} `)
  531. ? document.title
  532. : `${icon} ${document.title}`
  533. : document.title.replace(new RegExp(`^${icon} `), '');
  534. }
  535.  
  536. observeUnseenYou(you) {
  537. you.classList.add('fcx-observe-you');
  538.  
  539. const observer = new IntersectionObserver((entries, observer) => {
  540. entries.forEach(entry => {
  541. if (entry.isIntersecting) {
  542. const id = you.id;
  543. you.classList.remove('fcx-observe-you');
  544.  
  545. if (!this.seenYous.includes(id)) {
  546. this.seenYous.push(id);
  547. localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous));
  548. }
  549.  
  550. observer.unobserve(you);
  551. this.updateYous();
  552.  
  553. }
  554. });
  555. }, { rootMargin: '0px', threshold: 0.001 });
  556.  
  557. observer.observe(you);
  558. }
  559.  
  560. setUnseenYous() {
  561. this.seenKey = `${this.threadId}-seen-yous`;
  562. this.seenYous = JSON.parse(localStorage.getItem(this.seenKey));
  563.  
  564. if (!this.seenYous) {
  565. this.seenYous = [];
  566. localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous));
  567. }
  568.  
  569. this.unseenYous = this.yous.filter(you => !this.seenYous.includes(you.id));
  570.  
  571. this.unseenYous.forEach(you => {
  572. if (!you.classList.contains('fcx-observe-you')) {
  573. this.observeUnseenYou(you);
  574. }
  575. });
  576. }
  577.  
  578. showMascot(mascotData) {
  579. let mascot = null;
  580.  
  581. if (mascotData) {
  582. mascot = mascotData;
  583. } else {
  584. const mascotList = this.settingsEl?.savedMascots
  585. .filter(mascot => mascot.enabled);
  586. if (!mascotList || mascotList.length === 0) return;
  587. mascot = mascotList[Math.floor(Math.random() * mascotList.length)];
  588. }
  589.  
  590. if (!mascot.image) return;
  591.  
  592. if (!this.mascotEl) {
  593. this.mascotEl = document.createElement('img');
  594. this.mascotEl.classList.add('fcx-mascot');
  595. document.body.appendChild(this.mascotEl);
  596. }
  597.  
  598. this.mascotEl.style = "";
  599. this.mascotEl.src = mascot.image;
  600. this.mascotEl.style.opacity = this.settingsMascot.opacity * 0.01;
  601.  
  602. if (mascot.top) this.mascotEl.style.top = mascot.top;
  603. if (mascot.left) this.mascotEl.style.left = mascot.left;
  604. if (mascot.right) this.mascotEl.style.right = mascot.right;
  605. if (mascot.bottom) this.mascotEl.style.bottom = mascot.bottom;
  606.  
  607. if (mascot.width) this.mascotEl.style.width = mascot.width;
  608. if (mascot.height) this.mascotEl.style.height = mascot.height;
  609. if (mascot.flipImage) this.mascotEl.style.transform = 'scaleX(-1)';
  610. }
  611. };
  612.  
  613. window.customElements.define('fullchan-x', fullChanX);
  614.  
  615.  
  616. class fullChanXSettings extends HTMLElement {
  617. constructor() {
  618. super();
  619. this.settingsKey = 'fullchan-x-settings';
  620. this.mascotKey = 'fullchan-x-mascots';
  621. this.inputs = [];
  622. this.settings = {};
  623. this.settingsTemplate = {
  624. main: {
  625. moveToNav: {
  626. info: 'Move Fullchan-X controls into the navbar.',
  627. type: 'checkbox',
  628. value: true
  629. },
  630. enableHeaderMenu: {
  631. info: "Replaces popup settings in header with dropdown menu",
  632. type: 'checkbox',
  633. value: false
  634. },
  635. enableEnhancedReplies: {
  636. 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>",
  637. type: 'checkbox',
  638. value: false
  639. },
  640. enableIconBacklinks: {
  641. info: "Display reply backlinks as icons.",
  642. type: 'checkbox',
  643. value: false
  644. },
  645. hideDeletionBox: {
  646. info: "Not much point in seeing this if you're not an mod.",
  647. type: 'checkbox',
  648. value: false
  649. },
  650. enableIdPostList: {
  651. info: "Show list of posts when clicking an ID.",
  652. type: 'checkbox',
  653. value: true
  654. },
  655. doNotShowLocation: {
  656. info: "Board with location option will be set to false by default.",
  657. type: 'checkbox',
  658. value: false
  659. },
  660. enableFileExtensions: {
  661. info: 'Always show filetype on shortened file names.',
  662. type: 'checkbox',
  663. value: true
  664. },
  665. customBoardLinks: {
  666. info: 'List of custom boards in nav (seperate by comma)',
  667. type: 'input',
  668. value: 'v,a,b'
  669. },
  670. hideDefaultBoards: {
  671. info: 'List of boards to remove from nav (seperate by comma). Set as "all" to remove all.',
  672. type: 'input',
  673. value: 'interracial,mlp'
  674. },
  675. catalogBoardLinks: {
  676. info: 'Redirect nav board links to catalog pages.',
  677. type: 'checkbox',
  678. value: true
  679. },
  680. uiTopPosition: {
  681. info: 'Position from top of screen e.g. 100px',
  682. type: 'input',
  683. value: '50px'
  684. },
  685. uiRightPosition: {
  686. info: 'Position from right of screen e.g. 100px',
  687. type: 'input',
  688. value: '25px'
  689. },
  690. uiDimWhenInactive: {
  691. info: 'Dim UI when not hovering with mouse.',
  692. type: 'checkbox',
  693. value: true
  694. },
  695. hideNavbar: {
  696. info: 'Hide navbar until hover.',
  697. type: 'checkbox',
  698. value: false
  699. },
  700. replyTabIcon: {
  701. info: 'Set the icon/text added to tab title when you get a new (You).',
  702. type: 'input',
  703. value: '❗'
  704. }
  705. },
  706. mascot: {
  707. enableMascot: {
  708. info: 'Enable mascot image.',
  709. type: 'checkbox',
  710. value: false
  711. },
  712. enableMascotAddButtons: {
  713. info: 'Add mascots-add button to post images.',
  714. type: 'checkbox',
  715. value: true
  716. },
  717. opacity: {
  718. info: 'Opacity (1 to 100)',
  719. type: 'input',
  720. inputType: 'number',
  721. value: '75'
  722. }
  723. },
  724. mascotImage: {
  725. id: {
  726. type: 'input',
  727. value: '',
  728. },
  729. enabled: {
  730. info: 'Enable this mascot.',
  731. type: 'checkbox',
  732. value: true
  733. },
  734. name: {
  735. info: 'Descriptive name',
  736. type: 'input',
  737. value: 'New Mascot'
  738. },
  739. image: {
  740. info: 'Image URL (8chan image recommended).',
  741. type: 'input',
  742. value: '/.static/logo.png'
  743. },
  744. flipImage: {
  745. info: 'Mirror the mascot image.',
  746. type: 'checkbox',
  747. value: false
  748. },
  749. width: {
  750. info: 'Width of image.',
  751. type: 'input',
  752. value: '300px'
  753. },
  754. height: {
  755. info: 'Height of image.',
  756. type: 'input',
  757. value: 'auto'
  758. },
  759. bottom: {
  760. info: 'Bottom position.',
  761. type: 'input',
  762. value: '0px'
  763. },
  764. right: {
  765. info: 'Right position.',
  766. type: 'input',
  767. value: '0px'
  768. },
  769. top: {
  770. info: 'Top position.',
  771. type: 'input',
  772. value: ''
  773. },
  774. left: {
  775. info: 'Left position.',
  776. type: 'input',
  777. value: ''
  778. }
  779. },
  780. threadBanisher: {
  781. enableThreadBanisher: {
  782. info: 'Banish shit threads to the bottom of the calalog.',
  783. type: 'checkbox',
  784. value: true
  785. },
  786. boards: {
  787. info: 'Banish theads on these boards (seperated by comma).',
  788. type: 'input',
  789. value: 'v,a'
  790. },
  791. minimumCharacters: {
  792. info: 'Minimum character requirements',
  793. type: 'input',
  794. inputType: 'number',
  795. value: 100
  796. },
  797. banishTerms: {
  798. info: `<p>Banish threads with these terms to the bottom of the catalog (new line per term).</p>
  799. <p>How to use regex: <a href="https://www.datacamp.com/cheat-sheet/regular-expresso" target="__blank">Regex Cheatsheet</a>.</p>
  800. <p>NOTE: word breaks (\\b) MUST be entered as double escapes (\\\\b), they will appear as (\\b) when saved.</p>
  801. `,
  802. type: 'textarea',
  803. 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'
  804. },
  805. whitelistCyclical: {
  806. info: 'Whitelist cyclical threads.',
  807. type: 'checkbox',
  808. value: true
  809. },
  810. banishAnchored: {
  811. info: 'Banish anchored threads that are under minimum reply count.',
  812. type: 'checkbox',
  813. value: true
  814. },
  815. whitelistReplyCount: {
  816. info: 'Threads above this reply count (excluding those with banish terms) will be whitelisted.',
  817. type: 'input',
  818. inputType: 'number',
  819. value: 100
  820. },
  821. },
  822. defaultMascot: {
  823. enabled: true,
  824. id: '',
  825. name: 'New Mascot',
  826. image: '/.static/logo.png',
  827. flipImage: false,
  828. width: '300px',
  829. height: 'auto',
  830. bottom: '0px',
  831. right: '0px',
  832. top: '',
  833. left: '',
  834. }
  835. };
  836. }
  837.  
  838. async init() {
  839. this.fcx = document.querySelector('fullchan-x');
  840. this.settingsMainEl = this.querySelector('.fcxs-main');
  841. this.settingsThreadBanisherEl = this.querySelector('.fcxs-thread-banisher');
  842. this.settingsMascotEl = this.querySelector('.fcxs-mascot-settings');
  843. this.mascotListEl = this.querySelector('.fcxs-mascot-list');
  844. this.mascotSettingsTemplate = {...this.settingsTemplate.mascotImage};
  845. this.currentMascotSettings = {...this.settingsTemplate.defaultMascot};
  846.  
  847. this.addMascotEl = this.querySelector('.fcxs-add-mascot-settings');
  848. this.saveMascotButton = this.querySelector('.fcxs-save-mascot');
  849.  
  850. await this.getSavedSettings();
  851. await this.getSavedMascots();
  852.  
  853. if (this.settings.main) {
  854. this.fcx.init();
  855. this.loaded = true;
  856. };
  857.  
  858. this.buildSettingsOptions('main', 'settings', this.settingsMainEl);
  859. this.buildSettingsOptions('threadBanisher', 'settings', this.settingsThreadBanisherEl);
  860. this.buildSettingsOptions('mascot', 'settings', this.settingsMascotEl);
  861. this.buildSettingsOptions('mascotImage', 'mascotSettingsTemplate', this.addMascotEl);
  862.  
  863. this.listeners();
  864. this.querySelector('.fcx-settings__close').addEventListener('click', () => this.close());
  865.  
  866. document.body.classList.toggle('fcx-add-mascot-button', this.settings.mascot.enableMascotAddButtons);
  867.  
  868. if (!this.loaded) this.fcx.init();
  869. }
  870.  
  871. getRandomId () {
  872. return `id${Math.random().toString(36).substring(2, 8)}`;
  873. }
  874.  
  875. async setSavedSettings(settingsKey, status) {
  876. console.log("SAVING", this.settings);
  877. await GM.setValue(settingsKey, JSON.stringify(this.settings));
  878. if (status === 'updated') this.classList.add('fcxs-updated');
  879. }
  880.  
  881. async getSavedSettings() {
  882. let saved = JSON.parse(await GM.getValue(this.settingsKey, 'null'));
  883.  
  884. if (!saved) {
  885. const localSaved = JSON.parse(localStorage.getItem(this.settingsKey));
  886. if (localSaved) {
  887. saved = localSaved;
  888. await GM.setValue(this.settingsKey, JSON.stringify(saved));
  889. localStorage.removeItem(this.settingsKey);
  890. console.log('[Fullchan-X] Migrated settings from localStorage to GM storage.');
  891. }
  892. }
  893.  
  894. if (!saved) return;
  895.  
  896. let migrated = false;
  897. for (const [sectionKey, sectionTemplate] of Object.entries(this.settingsTemplate)) {
  898. if (!saved[sectionKey]) {
  899. saved[sectionKey] = {};
  900. }
  901. for (const [key, defaultConfig] of Object.entries(sectionTemplate)) {
  902. if (saved[sectionKey][key] && typeof saved[sectionKey][key] === 'object' && 'value' in saved[sectionKey][key]) {
  903. // Old format detected, migrating it
  904. saved[sectionKey][key] = saved[sectionKey][key].value;
  905. migrated = true;
  906. }
  907. }
  908. }
  909.  
  910. this.settings = saved;
  911. if (migrated) {
  912. console.log('[Fullchan-X] Migrated old settings to new format.');
  913. this.setSavedSettings(this.settingsKey, 'migrated');
  914. }
  915.  
  916. console.log('SAVED SETTINGS:', this.settings)
  917. }
  918.  
  919. async updateSavedMascot(mascot, status = 'updated') {
  920. const index = this.savedMascots.findIndex(objectMascot => objectMascot.id === mascot.id);
  921. if (index !== -1) {
  922. this.savedMascots[index] = mascot;
  923. } else {
  924. this.savedMascots.push(mascot);
  925. }
  926. await GM.setValue(this.mascotKey, JSON.stringify(this.savedMascots));
  927. this.classList.add(`fcxs-mascot-${status}`);
  928. }
  929.  
  930. async getSavedMascots() {
  931. let savedMascots = JSON.parse(await GM.getValue(this.mascotKey, 'null'));
  932.  
  933. if (!savedMascots) {
  934. const localSaved = JSON.parse(localStorage.getItem(this.mascotKey));
  935. if (localSaved) {
  936. savedMascots = localSaved;
  937. await GM.setValue(this.mascotKey, JSON.stringify(savedMascots));
  938. localStorage.removeItem(this.mascotKey);
  939. console.log('[Fullchan-X] Migrated mascots from localStorage to GM storage.');
  940. }
  941. }
  942.  
  943. if (!(savedMascots?.length > 0)) {
  944. savedMascots = [
  945. {
  946. ...this.settingsTemplate.defaultMascot,
  947. name: 'Vivian',
  948. id: 'id0',
  949. image: '/.media/4283cdb87bc82b2617509306c6a50bd9d6d015f727f931fb4969b499508e2e7e.webp'
  950. }
  951. ];
  952. }
  953.  
  954. this.savedMascots = savedMascots;
  955. this.savedMascots.forEach(mascot => this.addMascotCard(mascot));
  956. }
  957.  
  958. addMascotCard(mascot, replaceId) {
  959. const card = document.createElement('div');
  960. card.classList = `fcxs-mascot-card${mascot.enabled?'':' fcxs-mascot-card--disabled'}`;
  961. card.id = mascot.id;
  962. card.innerHTML = `
  963. <img src="${mascot.image}" loading="lazy">
  964. <div class="fcxs-mascot-card__name">
  965. <span>${mascot.name}</span>
  966. </div>
  967. <div class="fcxs-mascot-card__buttons">
  968. <button class="fcxs-mascot-card__button" name="edit">Edit</button>
  969. <button class="fcxs-mascot-card__button" name="delete">Delete</button>
  970. </div>
  971. `;
  972. if (replaceId) {
  973. const oldCard = this.mascotListEl.querySelector(`#${replaceId}`);
  974. if (oldCard) {
  975. this.mascotListEl.replaceChild(card, oldCard);
  976. return;
  977. }
  978. }
  979. this.mascotListEl.appendChild(card);
  980. }
  981.  
  982. addMascotFromPost(imageUrl, imageName, fakeButtonEl) {
  983. const acceptedTypes = ['jpeg', 'jpg', 'gif', 'png', 'webp'];
  984. const noneTransparentTypes = ['jpeg', 'jpg'];
  985. const fileType = imageUrl.split('.').pop().toLowerCase();
  986.  
  987. if (!acceptedTypes.includes(fileType)) {
  988. window.alert('This file type cannot be used as a mascot.');
  989. return;
  990. }
  991.  
  992. try {
  993. const mascotUrl = imageUrl.includes('/.media/')
  994. ? '/.media/' + imageUrl.split('/.media/')[1]
  995. : imageUrl;
  996.  
  997. this.currentMascotSettings = {
  998. ...this.settingsTemplate.defaultMascot,
  999. image: mascotUrl,
  1000. name: imageName
  1001. };
  1002.  
  1003. this.handleSaveMascot();
  1004. fakeButtonEl.classList.add('mascotAdded');
  1005.  
  1006. if (noneTransparentTypes.includes(fileType)) {
  1007. window.alert('Mascot added, but this file type does not support transparency.');
  1008. }
  1009. } catch (error) {
  1010. console.error('Error adding mascot:', error);
  1011. window.alert('Failed to add mascot. Please try again.');
  1012. }
  1013. }
  1014.  
  1015. async handleSaveMascot(event) {
  1016. const mascot = { ...this.currentMascotSettings };
  1017. if (!mascot.id) mascot.id = this.getRandomId();
  1018. const index = this.savedMascots.findIndex(m => m.id === mascot.id);
  1019.  
  1020. if (index !== -1) {
  1021. this.savedMascots[index] = mascot;
  1022. this.addMascotCard(mascot, mascot.id);
  1023. } else {
  1024. this.savedMascots.push(mascot);
  1025. this.addMascotCard(mascot);
  1026. }
  1027.  
  1028. await GM.setValue(this.mascotKey, JSON.stringify(this.savedMascots));
  1029. this.classList.remove('fcxs--mascot-modal');
  1030. }
  1031.  
  1032. async handleMascotClick(clicked, event) {
  1033. const mascotEl = clicked.closest('.fcxs-mascot-card');
  1034. if (!mascotEl) return;
  1035. const mascotId = mascotEl.id;
  1036. const mascot = this.savedMascots.find(m => m.id === mascotId);
  1037. const button = clicked.closest('.fcxs-mascot-card__button');
  1038. const mascotTitle = clicked.closest('.fcxs-mascot-card__name');
  1039. const mascotImg = clicked.closest('img');
  1040.  
  1041. if (mascotTitle) {
  1042. this.fcx.showMascot(mascot);
  1043. } else if (mascotImg) {
  1044. const updatedMascot = {...mascot, enabled: !mascot.enabled}
  1045. this.currentMascotSettings = {...updatedMascot};
  1046. this.handleSaveMascot();
  1047. this.addMascotCard(updatedMascot, mascotId);
  1048. } else if (button) {
  1049. const buttonType = button.name;
  1050. if (buttonType === 'delete') {
  1051. this.savedMascots = this.savedMascots.filter(m => m.id !== mascotId);
  1052. await GM.setValue(this.mascotKey, JSON.stringify(this.savedMascots));
  1053. mascotEl.remove();
  1054. } else if (buttonType === 'edit') {
  1055. if (!mascot) return;
  1056. this.classList.add('fcxs--mascot-modal');
  1057. this.saveMascotButton.disabled = true;
  1058. this.currentMascotSettings = {...mascot}
  1059. for (const key of Object.keys(this.currentMascotSettings)) {
  1060. if (mascot[key] !== undefined) {
  1061. const input = this.addMascotEl.querySelector(`[name="${key}"]`);
  1062. if (input) {
  1063. if (input.type === 'checkbox') {
  1064. input.checked = mascot[key];
  1065. } else {
  1066. input.value = mascot[key];
  1067. }
  1068. }
  1069. }
  1070. }
  1071. }
  1072. }
  1073. }
  1074.  
  1075. handleClick(event) {
  1076. const clicked = event.target;
  1077. if (clicked.closest('.fcxs-mascot-card')) this.handleMascotClick(clicked, event);
  1078. if (clicked.closest('.fcxs-close-mascot')) this.classList.remove('fcxs--mascot-modal');
  1079.  
  1080. if (clicked.closest('.fcxs-mascot__new')) {
  1081. this.currentMascotSettings = {...this.settingsTemplate.defaultMascot};
  1082. const mascot = this.currentMascotSettings;
  1083. for (const key of Object.keys(this.currentMascotSettings)) {
  1084. if (mascot[key] !== undefined) {
  1085. const input = this.addMascotEl.querySelector(`[name="${key}"]`);
  1086. if (input) {
  1087. if (input.type === 'checkbox') {
  1088. input.checked = mascot[key];
  1089. } else {
  1090. input.value = mascot[key];
  1091. }
  1092. }
  1093. }
  1094. this.classList.add('fcxs--mascot-modal');
  1095. this.saveMascotButton.disabled = true;
  1096. }
  1097. }
  1098. }
  1099.  
  1100. listeners() {
  1101. this.saveMascotButton.addEventListener('click', event => this.handleSaveMascot(event));
  1102. this.addEventListener('click', event => this.handleClick(event));
  1103.  
  1104. this.inputs.forEach(input => {
  1105. input.addEventListener('change', () => {
  1106. const settingsKey = input.dataset.settingsKey;
  1107. if (settingsKey === 'mascotImage') {
  1108. const value = input.type === 'checkbox' ? input.checked : input.value;
  1109. this.currentMascotSettings[input.name] = value;
  1110. this.saveMascotButton.disabled = false;
  1111. this.fcx.showMascot(this.currentMascotSettings);
  1112. return;
  1113. }
  1114.  
  1115. const settingsObject = this.settings[settingsKey];
  1116. const key = input.name;
  1117. const value = input.type === 'checkbox' ? input.checked : input.value;
  1118.  
  1119. settingsObject[key] = value;
  1120. this.setSavedSettings(this.settingsKey, 'updated');
  1121. });
  1122. });
  1123. }
  1124.  
  1125. buildSettingsOptions(settingsKey, parentKey, parent) {
  1126. if (!parent) return;
  1127.  
  1128. if (!this[parentKey][settingsKey]) this[parentKey][settingsKey] = {...this.settingsTemplate[settingsKey]};
  1129. const settingsObject = this[parentKey][settingsKey];
  1130.  
  1131. Object.entries(this.settingsTemplate[settingsKey]).forEach(([key, config]) => {
  1132.  
  1133. if (typeof settingsObject[key] === 'undefined') {
  1134. settingsObject[key] = config.value ?? ''; // God f***ing damn the hell that not having this caused me. Yes, I am r*****ed.
  1135. }
  1136.  
  1137. const wrapper = document.createElement('div');
  1138. const infoWrapper = document.createElement('div');
  1139. wrapper.classList = (`fcx-setting fcx-setting--${key}`);
  1140. infoWrapper.classList.add('fcx-setting__info');
  1141. wrapper.appendChild(infoWrapper);
  1142.  
  1143. const label = document.createElement('label');
  1144. label.textContent = key
  1145. .replace(/([A-Z])/g, ' $1')
  1146. .replace(/^./, str => str.toUpperCase());
  1147. label.setAttribute('for', key);
  1148. infoWrapper.appendChild(label);
  1149.  
  1150. if (config.info) {
  1151. const info = document.createElement('p');
  1152. info.innerHTML = config.info;
  1153. infoWrapper.appendChild(info);
  1154. }
  1155.  
  1156. let savedValue = settingsObject[key].value ?? settingsObject[key] ?? config.value;
  1157. if (settingsObject[key]?.value) savedValue = settingsObject[key].value;
  1158.  
  1159. let input;
  1160.  
  1161. if (config.type === 'checkbox') {
  1162. input = document.createElement('input');
  1163. input.type = 'checkbox';
  1164. input.checked = savedValue;
  1165. } else if (config.type === 'textarea') {
  1166. input = document.createElement('textarea');
  1167. input.value = savedValue;
  1168. } else if (config.type === 'input') {
  1169. input = document.createElement('input');
  1170. input.type = config.inputType || 'text';
  1171. input.value = savedValue;
  1172. } else if (config.type === 'select' && config.options) {
  1173. input = document.createElement('select');
  1174. const options = config.options.split(',');
  1175. options.forEach(opt => {
  1176. const option = document.createElement('option');
  1177. option.value = opt;
  1178. option.textContent = opt;
  1179. if (opt === savedValue) option.selected = true;
  1180. input.appendChild(option);
  1181. });
  1182. }
  1183.  
  1184. if (input) {
  1185. input.id = key;
  1186. input.name = key;
  1187. input.dataset.settingsKey = settingsKey;
  1188. wrapper.appendChild(input);
  1189. this.inputs.push(input);
  1190. settingsObject[key] = input.type === 'checkbox' ? input.checked : input.value;
  1191. }
  1192.  
  1193. parent.appendChild(wrapper);
  1194. });
  1195. }
  1196.  
  1197. open() {
  1198. this.classList.add('open');
  1199. }
  1200.  
  1201. close() {
  1202. this.classList.remove('open');
  1203. }
  1204.  
  1205. toggle() {
  1206. this.classList.toggle('open');
  1207. }
  1208. }
  1209.  
  1210. window.customElements.define('fullchan-x-settings', fullChanXSettings);
  1211.  
  1212.  
  1213.  
  1214. class fullChanXGallery extends HTMLElement {
  1215. constructor() {
  1216. super();
  1217. }
  1218.  
  1219. init() {
  1220. this.fullchanX = document.querySelector('fullchan-x');
  1221. this.imageContainer = this.querySelector('.gallery__images');
  1222. this.mainImageContainer = this.querySelector('.gallery__main-image');
  1223. this.mainImage = this.mainImageContainer.querySelector('img');
  1224. this.sizeButtons = [...this.querySelectorAll('.gallery__scale-options .scale-option')];
  1225. this.closeButton = this.querySelector('.gallery__close');
  1226. this.listeners();
  1227. this.addGalleryImages();
  1228. this.initalized = true;
  1229. }
  1230.  
  1231. addGalleryImages () {
  1232. this.thumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].map(thumb => {
  1233. return thumb.cloneNode(true);
  1234. });
  1235.  
  1236. this.thumbs.forEach(thumb => {
  1237. this.imageContainer.appendChild(thumb);
  1238. });
  1239. }
  1240.  
  1241. updateGalleryImages () {
  1242. if (!this.initalized) return;
  1243.  
  1244. const newThumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].filter(thumb => {
  1245. return !this.thumbs.find(thisThumb.href === thumb.href);
  1246. }).map(thumb => {
  1247. return thumb.cloneNode(true);
  1248. });
  1249.  
  1250. newThumbs.forEach(thumb => {
  1251. this.thumbs.push(thumb);
  1252. this.imageContainer.appendChild(thumb);
  1253. });
  1254. }
  1255.  
  1256. listeners () {
  1257. this.addEventListener('click', event => {
  1258. const clicked = event.target;
  1259.  
  1260. let imgLink = clicked.closest('.imgLink');
  1261. if (imgLink?.dataset.filemime === 'video/webm') return;
  1262.  
  1263. if (imgLink) {
  1264. event.preventDefault();
  1265. this.mainImage.src = imgLink.href;
  1266. }
  1267.  
  1268. this.mainImageContainer.classList.toggle('active', !!imgLink);
  1269.  
  1270. const scaleButton = clicked.closest('.scale-option');
  1271. if (scaleButton) {
  1272. const scale = parseFloat(getComputedStyle(this.imageContainer).getPropertyValue('--scale')) || 1;
  1273. const delta = scaleButton.id === 'fcxg-smaller' ? -0.1 : 0.1;
  1274. const newScale = Math.max(0.1, scale + delta);
  1275. this.imageContainer.style.setProperty('--scale', newScale.toFixed(2));
  1276. }
  1277.  
  1278. if (clicked.closest('.gallery__close')) this.close();
  1279. });
  1280. }
  1281.  
  1282. open () {
  1283. if (!this.initalized) this.init();
  1284. this.classList.add('open');
  1285. document.body.classList.add('fct-gallery-open');
  1286. }
  1287.  
  1288. close () {
  1289. this.classList.remove('open');
  1290. document.body.classList.remove('fct-gallery-open');
  1291. }
  1292. }
  1293.  
  1294. window.customElements.define('fullchan-x-gallery', fullChanXGallery);
  1295.  
  1296.  
  1297. class ToggleButton extends HTMLElement {
  1298. constructor() {
  1299. super();
  1300. const data = this.dataset;
  1301. this.onclick = () => {
  1302. const target = data.target ? document.querySelector(data.target) : this;
  1303. const value = data.value || 'active';
  1304. !!data.set ? target.dataset[data.set] = value : target.classList.toggle(value);
  1305. }
  1306. }
  1307. }
  1308.  
  1309. window.customElements.define('toggle-button', ToggleButton);
  1310.  
  1311.  
  1312.  
  1313. class FullChanXHeaderMenu extends HTMLElement {
  1314. constructor() {
  1315. super();
  1316. }
  1317.  
  1318. init() {
  1319. document.body.classList.add('fcx-header-menu');
  1320. this.menuContainer = this.querySelector('.fcxm__menu-container');
  1321. this.menuButtonsContainer = this.querySelector('.fcxm__menu-tabs');
  1322.  
  1323. this.menuIds = [
  1324. 'settingsMenu',
  1325. 'watchedMenu',
  1326. 'multiboardMenu',
  1327. ];
  1328.  
  1329. this.menuButtons = [...this.menuIds, 'helpMenu'].map(selector => {
  1330. const menuName = selector.replace('#','').replace('Menu','');
  1331. const button = document.createElement('button');
  1332. button.dataset.target = selector;
  1333. button.classList = `fcxm__menu-button fcxm__menu-button--${menuName}${menuName === 'settings' ? ' fcx-active' : ''}`;
  1334. button.innerHTML = `<span>${menuName}</span>`;
  1335. return button;
  1336. });
  1337. this.menuButtons.forEach(button => this.menuButtonsContainer.appendChild(button));
  1338.  
  1339. this.menus = this.menuIds.map(selector => {
  1340. return document.getElementById(selector);
  1341. });
  1342. this.menus[0]?.classList.add('fcx-active');
  1343.  
  1344. if (!this.menus[0]) return;
  1345.  
  1346. this.createHelpMenu();
  1347.  
  1348. this.menus.forEach(menu => {
  1349. this.menuContainer.appendChild(menu);
  1350. });
  1351.  
  1352. this.listeners();
  1353. }
  1354.  
  1355. listeners() {
  1356. this.menuButtons.forEach(button => {
  1357. button.addEventListener('mouseenter', () => this.toggleMenus(button));
  1358. });
  1359.  
  1360. this.menuTrigger = document.createElement('div');
  1361. this.menuTrigger.classList = 'fcx-menu-trigger';
  1362. this.mainNav = document.querySelector('#dynamicHeaderThread');
  1363. this.mainNav.appendChild(this.menuTrigger);
  1364.  
  1365.  
  1366. if (this.menuTrigger) {
  1367. let hideTimeout; // Declare it here
  1368.  
  1369. const activate = () => {
  1370. clearTimeout(hideTimeout);
  1371. this.classList.add('fcx-active');
  1372. this.menuTrigger.classList.add('fcx-active');
  1373. };
  1374.  
  1375. const scheduleDeactivate = () => {
  1376. hideTimeout = setTimeout(() => {
  1377. this.classList.remove('fcx-active');
  1378. this.menuTrigger.classList.remove('fcx-active');
  1379. }, 300);
  1380. };
  1381.  
  1382. [this.menuTrigger, this].forEach(el => {
  1383. console.log('el', el)
  1384. el.addEventListener('mouseenter', activate);
  1385. el.addEventListener('mouseleave', scheduleDeactivate);
  1386. });
  1387. }
  1388. }
  1389.  
  1390. toggleMenus(button) {
  1391. const toggleElements = [...this.menuButtons,...this.menus];
  1392. toggleElements.forEach(el => {
  1393. el.classList.toggle('fcx-active', el === button || el.id === button.dataset.target);
  1394. });
  1395. }
  1396.  
  1397. async createHelpMenu() {
  1398. this.helpMenu = document.createElement('div');
  1399. this.helpMenu.id = "helpMenu";
  1400. this.helpMenu.classList = 'floatingMenu';
  1401. this.menuContainer.appendChild(this.helpMenu);
  1402. this.menus.push(this.helpMenu);
  1403.  
  1404. const response = await fetch('/.static/pages/posting.html');
  1405. const htmlText = await response.text();
  1406. const tempDoc = new DOMParser().parseFromString(htmlText, 'text/html');
  1407. const titleWrapper = tempDoc.body.querySelector('.titleWrapper');
  1408. if (titleWrapper) this.helpMenu.innerHTML = `
  1409. <div class="fcxm__help-page-content">${titleWrapper.innerHTML}</div>
  1410. `;
  1411. }
  1412. }
  1413.  
  1414. window.customElements.define('fullchan-x-header-menu', FullChanXHeaderMenu);
  1415.  
  1416.  
  1417.  
  1418. // Create fullchan-x header menu
  1419. const fcxm = document.createElement('fullchan-x-header-menu');
  1420. fcxm.innerHTML = `
  1421. <div id="fcxm" class="fcxm">
  1422. <div class="fcxm__menu-tabs"></div>
  1423. <div class="fcxm__menu-container"></div>
  1424. </div>
  1425. `;
  1426. document.body.appendChild(fcxm);
  1427.  
  1428.  
  1429.  
  1430. // Create fullchan-x gallery
  1431. const fcxg = document.createElement('fullchan-x-gallery');
  1432. fcxg.innerHTML = `
  1433. <div class="fcxg gallery">
  1434. <button id="fcxg-close" class="gallery__close fullchan-x__option">Close</button>
  1435. <div class="gallery__scale-options">
  1436. <button id="fcxg-smaller" class="scale-option fullchan-x__option">-</button>
  1437. <button id="fcxg-larger" class="scale-option fullchan-x__option">+</button>
  1438. </div>
  1439. <div id="fcxg-images" class="gallery__images" style="--scale:1.0"></div>
  1440. <div id="fcxg-main-image" class="gallery__main-image">
  1441. <img src="" />
  1442. </div>
  1443. </div>
  1444. `;
  1445. document.body.appendChild(fcxg);
  1446.  
  1447.  
  1448.  
  1449. // Create fullchan-x element
  1450. const fcx = document.createElement('fullchan-x');
  1451. fcx.innerHTML = `
  1452. <div class="fcx__controls">
  1453. <button id="fcx-settings-btn" class="fullchan-x__option fcx-settings-toggle">
  1454. <a>⚙️</a><span>Settings</span>
  1455. </button>
  1456.  
  1457. <div class="fullchan-x__option fullchan-x__sort thread-only">
  1458. <a>☰</a>
  1459. <select id="thread-sort">
  1460. <option value="default">Default</option>
  1461. <option value="replies">Replies</option>
  1462. <option value="catbox">Catbox</option>
  1463. </select>
  1464. </div>
  1465.  
  1466. <button id="fcx-gallery-btn" class="gallery__toggle fullchan-x__option thread-only">
  1467. <a>🖼️</a><span>Gallery</span>
  1468. </button>
  1469.  
  1470. <div class="fcx__my-yous thread-only">
  1471. <p class="my-yous__label fullchan-x__option"><a>💬</a><span>My (You)s</span></p>
  1472. <div class="my-yous__yous fcx-prevent-nesting" id="my-yous"></div>
  1473. </div>
  1474. </div>
  1475. `;
  1476. (document.querySelector('.navHeader') || document.body).appendChild(fcx);
  1477.  
  1478.  
  1479.  
  1480. // Create fullchan-x settings
  1481. const fcxs = document.createElement('fullchan-x-settings');
  1482. fcxs.innerHTML = `
  1483. <div class="fcx-settings fcxs" data-tab="main">
  1484. <header>
  1485. <div class="fcxs__heading">
  1486. <span class="fcx-settings__title">
  1487. <img class="fcxs_logo" src="/.static/logo/logo_blue.png" height="25px" width="auto">
  1488. <span>
  1489. Fullchan-X Settings
  1490. </span>
  1491. </span>
  1492. <button class="fcx-settings__close fullchan-x__option">Close</button>
  1493. </div>
  1494.  
  1495. <div class="fcx-settings__tab-buttons">
  1496. <toggle-button data-target=".fcxs" data-set="tab" data-value="main">
  1497. Main
  1498. </toggle-button>
  1499. <toggle-button data-target=".fcxs" data-set="tab" data-value="catalog">
  1500. catalog
  1501. </toggle-button>
  1502. <toggle-button data-target=".fcxs" data-set="tab" data-value="mascot">
  1503. Mascot
  1504. </toggle-button>
  1505. </div>
  1506. </header>
  1507.  
  1508. <main>
  1509. <div class="fcxs__updated-message">
  1510. <p>Settings updated, refresh page to apply</p>
  1511. <button class="fullchan-x__option" onClick="location.reload()">Reload page</button>
  1512. </div>
  1513.  
  1514. <div class="fcx-settings__settings">
  1515. <div class="fcxs-main fcxs-tab"></div>
  1516. <div class="fcxs-mascot fcxs-tab">
  1517. <div class="fcxs-mascot-settings"></div>
  1518. <div class="fcxs-mascot-list">
  1519. <div class="fcxs-mascot__new">
  1520. <span>+</span>
  1521. </div>
  1522. </div>
  1523.  
  1524. <p class="fcxs-tab__description">
  1525. Go to <a href="/mascot/catalog.html" target="__blank">8chan.*/mascot/</a> to store or find new mascots.
  1526. </p>
  1527. </div>
  1528. <div class="fcxs-catalog fcxs-tab">
  1529. <div class="fcxs-thread-banisher"></div>
  1530. </div>
  1531. </div>
  1532. </main>
  1533.  
  1534. <footer>
  1535. </footer>
  1536. </div>
  1537.  
  1538. <div class="fcxs-add-mascot">
  1539. <button class="fcx-option fcxs-close-mascot">Close</button>
  1540. <div class="fcxs-add-mascot-settings"></div>
  1541. <button class="fcx-option fcxs-save-mascot" disabled>Save Mascot</button>
  1542. </div>
  1543. `;
  1544.  
  1545.  
  1546. // Styles
  1547. const style = document.createElement('style');
  1548. style.innerHTML = `
  1549. .fcx-hide-navboard #navTopBoardsSpan#navTopBoardsSpan#navTopBoardsSpan {
  1550. display: none!important;
  1551. }
  1552.  
  1553. fullchan-x {
  1554. --top: 50px;
  1555. --right: 25px;
  1556. background: var(--background-color);
  1557. border: 1px solid var(--navbar-text-color);
  1558. color: var(--link-color);
  1559. font-size: 14px;
  1560. z-index: 3;
  1561. }
  1562.  
  1563. toggle-button {
  1564. cursor: pointer;
  1565. }
  1566.  
  1567. /* Fullchan-X in nav styles */
  1568. .fcx-in-nav {
  1569. padding: 0;
  1570. border-width: 0;
  1571. line-height: 20px;
  1572. margin-right: 2px;
  1573. background: none;
  1574. }
  1575.  
  1576. .fcx-in-nav .fcx__controls:before,
  1577. .fcx-in-nav .fcx__controls:after {
  1578. color: var(--navbar-text-color);
  1579. font-size: 85%;
  1580. }
  1581.  
  1582. .fcx-in-nav .fcx__controls:before {
  1583. content: "]";
  1584. }
  1585.  
  1586. .fcx-in-nav .fcx__controls:after {
  1587. content: "[";
  1588. }
  1589.  
  1590. .fcx-in-nav .fcx__controls,
  1591. .fcx-in-nav:hover .fcx__controls:hover {
  1592. flex-direction: row-reverse;
  1593. }
  1594.  
  1595. .fcx-in-nav .fcx__controls .fullchan-x__option {
  1596. padding: 0!important;
  1597. justify-content: center;
  1598. background: none;
  1599. line-height: 0;
  1600. max-width: 20px;
  1601. min-width: 20px;
  1602. translate: 0 1px;
  1603. border: solid var(--navbar-text-color) 1px !important;
  1604. }
  1605.  
  1606. .fcx-in-nav .fcx__controls .fullchan-x__option:hover {
  1607. border: solid var(--subject-color) 1px !important;
  1608. }
  1609.  
  1610. .fcx-in-nav .fullchan-x__sort > a {
  1611. position: relative;
  1612. margin-bottom: 1px;
  1613. }
  1614.  
  1615. .fcx-in-nav .fcx__controls > * {
  1616. position: relative;
  1617. }
  1618.  
  1619. .fcx-in-nav .fcx__controls .fullchan-x__option > span,
  1620. .fcx-in-nav .fcx__controls .fullchan-x__option:not(:hover) > select {
  1621. display: none;
  1622. }
  1623.  
  1624. .fcx-in-nav .fcx__controls .fullchan-x__option > select {
  1625. appearance: none;
  1626. position: absolute;
  1627. left: 0;
  1628. top: 0;
  1629. width: 100%;
  1630. height: 100%;
  1631. font-size: 0;
  1632. }
  1633.  
  1634. .fcx-in-nav .fcx__controls .fullchan-x__option > select option {
  1635. font-size: 12px;
  1636. }
  1637.  
  1638. .fcx-in-nav .my-yous__yous {
  1639. position: absolute;
  1640. left: 50%;
  1641. translate: -50%;
  1642. background: var(--background-color);
  1643. border: 1px solid var(--navbar-text-color);
  1644. padding: 14px;
  1645. }
  1646.  
  1647. .bottom-header .fcx-in-nav .my-yous__yous {
  1648. top: 0;
  1649. translate: -50% -100%;
  1650. }
  1651.  
  1652. /* Fullchan-X main styles */
  1653. fullchan-x:not(.fcx-in-nav) {
  1654. top: var(--top);
  1655. right: var(--right);
  1656. display: block;
  1657. padding: 10px;
  1658. position: fixed;
  1659. display: block;
  1660. }
  1661.  
  1662. fullchan-x:not(.fcx-page-thread) .thread-only,
  1663. fullchan-x:not(.fcx-page-catalog) .catalog-only {
  1664. display: none!important;
  1665. }
  1666.  
  1667. fullchan-x:hover {
  1668. z-index: 1000!important;
  1669. }
  1670.  
  1671. .navHeader:has(fullchan-x:hover) {
  1672. z-index: 1000!important;
  1673. }
  1674.  
  1675. fullchan-x.fcx--dim:not(:hover) {
  1676. opacity: 0.6;
  1677. }
  1678.  
  1679. .divPosts {
  1680. flex-direction: column;
  1681. }
  1682.  
  1683. .fcx__controls {
  1684. display: flex;
  1685. flex-direction: column;
  1686. gap: 6px;
  1687. }
  1688.  
  1689. fullchan-x:not(:hover):not(:has(select:focus)) span,
  1690. fullchan-x:not(:hover):not(:has(select:focus)) select {
  1691. display: none;
  1692. margin-left: 5px;
  1693. z-index:3;
  1694. }
  1695.  
  1696. .fcx__controls span,
  1697. .fcx__controls select {
  1698. margin-left: 5px;
  1699. }
  1700.  
  1701. .fcx__controls select {
  1702. cursor: pointer;
  1703. }
  1704.  
  1705. #thread-sort {
  1706. border: none;
  1707. background: none;
  1708. }
  1709.  
  1710. .my-yous__yous {
  1711. display: none;
  1712. flex-direction: column;
  1713. padding-top: 10px;
  1714. max-height: calc(100vh - 220px - var(--top));
  1715. overflow: auto;
  1716. }
  1717.  
  1718. .fcx__my-yous:hover .my-yous__yous {
  1719. display: flex;
  1720. }
  1721.  
  1722. .fullchan-x__option,
  1723. .fcx-option {
  1724. display: flex;
  1725. padding: 6px 8px;
  1726. background: white;
  1727. border: none !important;
  1728. border-radius: 0.2rem;
  1729. transition: all ease 150ms;
  1730. cursor: pointer;
  1731. margin: 0;
  1732. font-weight: 400;
  1733. text-align: left;
  1734. min-width: 18px;
  1735. min-height: 18px;
  1736. align-items: center;
  1737. color: #374369;
  1738. }
  1739.  
  1740. .fullchan-x__option,
  1741. .fullchan-x__option select {
  1742. font-size: 12px;
  1743. font-weight: 400;
  1744. color: #374369;
  1745. }
  1746.  
  1747. fullchan-x:not(:hover):not(:has(select:focus)) .fullchan-x__option {
  1748. display: flex;
  1749. justify-content: center;
  1750. }
  1751.  
  1752. #thread-sort {
  1753. padding-right: 0;
  1754. }
  1755.  
  1756. #thread-sort:hover {
  1757. display: block;
  1758. }
  1759.  
  1760. .innerPost:has(>.divMessage>.you),
  1761. .innerPost:has(>.divMessage>*:not(div)>.you),
  1762. .innerPost:has(>.divMessage>*:not(div)>*:not(div)>.you) {
  1763. border-left: solid #dd003e 3px;
  1764. }
  1765.  
  1766. .innerPost.innerPost:has(>.postInfo>.youName),
  1767. .innerOP.innerOP:has(>.postInfo>.youName) {
  1768. border-left: solid #68b723 3px;
  1769. }
  1770.  
  1771. /* --- Nested quotes --- */
  1772. .divMessage .nestedPost {
  1773. display: inline-block;
  1774. width: 100%;
  1775. margin-bottom: 14px;
  1776. white-space: normal!important;
  1777. overflow-wrap: anywhere;
  1778. margin-top: 0.5em;
  1779. border: 1px solid var(--navbar-text-color);
  1780. }
  1781.  
  1782. .nestedPost .innerPost,
  1783. .nestedPost .innerOP {
  1784. width: 100%;
  1785. }
  1786.  
  1787. .nestedPost .imgLink .imgExpanded {
  1788. width: auto!important;
  1789. height: auto!important;
  1790. }
  1791.  
  1792. .my-yous__label.fcx-unseen {
  1793. background: var(--link-hover-color)!important;
  1794. color: white;
  1795. }
  1796.  
  1797. .my-yous__yous .fcx-unseen {
  1798. font-weight: 900;
  1799. color: var(--link-hover-color);
  1800. }
  1801.  
  1802. .panelBacklinks a.fcx-active {
  1803. color: #dd003e;
  1804. }
  1805.  
  1806. /*--- Settings --- */
  1807. fullchan-x-settings {
  1808. color: var(--link-color);
  1809. font-size: 14px;
  1810. }
  1811.  
  1812. .fcx-settings {
  1813. display: block;
  1814. position: fixed;
  1815. top: 50vh;
  1816. left: 50vw;
  1817. translate: -50% -50%;
  1818. padding: 0 0 20px;
  1819. background: var(--background-color);
  1820. border: 1px solid var(--navbar-text-color);
  1821. border-radius: 8px;
  1822. max-width: 480px;
  1823. max-height: 80vh;
  1824. overflow: auto;
  1825. min-width: 500px;
  1826. z-index: 1000;
  1827. }
  1828.  
  1829. .fcx-settings header {
  1830. position: sticky;
  1831. top: 0;
  1832. padding-top: 20px;
  1833. background: var(--background-color);
  1834. z-index: 3;
  1835. }
  1836.  
  1837. fullchan-x-settings:not(.open) {
  1838. display: none;
  1839. }
  1840.  
  1841. .fcxs__heading,
  1842. .fcxs-tab,
  1843. .fcxs footer {
  1844. padding: 0 20px;
  1845. }
  1846.  
  1847. .fcx-settings header {
  1848. margin: 0 0 15px;
  1849. border-bottom: 1px solid var(--navbar-text-color);
  1850. }
  1851.  
  1852. .fcxs__heading {
  1853. display: flex;
  1854. align-items: center;
  1855. justify-content: space-between;
  1856. padding-bottom: 20px;
  1857. }
  1858.  
  1859. .fcx-settings__title {
  1860. display: flex;
  1861. align-items: center;
  1862. gap: 10px;
  1863. font-size: 24px;
  1864. font-size: 24px;
  1865. letter-spacing: 0.04em;
  1866. }
  1867.  
  1868. .fcx-settings input[type="checkbox"] {
  1869. cursor: pointer;
  1870. }
  1871.  
  1872. .fcxs_logo {
  1873. .margin-top: -2px;
  1874. }
  1875.  
  1876. .fcx-settings__tab-buttons {
  1877. border-top: 1px solid var(--navbar-text-color);
  1878. display: flex;
  1879. align-items: center;
  1880. }
  1881.  
  1882. .fcx-settings__tab-buttons toggle-button {
  1883. flex: 1;
  1884. padding: 15px;
  1885. font-size: 14px;
  1886. }
  1887.  
  1888. .fcx-settings__tab-buttons toggle-button + toggle-button {
  1889. border-left: 1px solid var(--navbar-text-color);
  1890. }
  1891.  
  1892. .fcx-settings__tab-buttons toggle-button:hover {
  1893. color: var(--role-color);
  1894. }
  1895.  
  1896. fullchan-x-settings:not(.fcxs-updated) .fcxs__updated-message {
  1897. display: none;
  1898. }
  1899.  
  1900. .fcxs:not([data-tab="main"]) .fcxs-main,
  1901. .fcxs:not([data-tab="catalog"]) .fcxs-catalog,
  1902. .fcxs:not([data-tab="mascot"]) .fcxs-mascot {
  1903. display: none;
  1904. }
  1905.  
  1906. .fcxs[data-tab="main"] [data-value="main"],
  1907. .fcxs[data-tab="catalog"] [data-value="catalog"],
  1908. .fcxs[data-tab="mascot"] [data-value="mascot"] {
  1909. font-weight: 700;
  1910. }
  1911.  
  1912. .fcx-setting {
  1913. display: flex;
  1914. justify-content: space-between;
  1915. align-items: center;
  1916. padding: 12px 0;
  1917. }
  1918.  
  1919. .fcx-setting__info {
  1920. max-width: 60%;
  1921. }
  1922.  
  1923. .fcx-setting input[type="text"],
  1924. .fcx-setting input[type="number"],
  1925. .fcx-setting select,
  1926. .fcx-setting textarea {
  1927. padding: 4px 6px;
  1928. min-width: 35%;
  1929. }
  1930.  
  1931. .fcx-setting textarea {
  1932. min-height: 100px;
  1933. }
  1934.  
  1935. .fcx-setting label {
  1936. font-weight: 600;
  1937. }
  1938.  
  1939. .fcx-setting p {
  1940. margin: 6px 0 0;
  1941. font-size: 12px;
  1942. }
  1943.  
  1944. .fcx-setting + .fcx-setting {
  1945. border-top: 1px solid var(--navbar-text-color);
  1946. }
  1947.  
  1948. .fcxs__updated-message {
  1949. margin: 10px 0;
  1950. text-align: center;
  1951. }
  1952.  
  1953. .fcxs__updated-message p {
  1954. font-size: 14px;
  1955. color: var(--error);
  1956. }
  1957.  
  1958. .fcxs__updated-message button {
  1959. margin: 14px auto 0;
  1960. }
  1961.  
  1962. .fcxs-tab__description {
  1963. text-align: center;
  1964. margin-top: 24px;
  1965. font-size: 12px;
  1966. }
  1967.  
  1968. .fcxs-tab__description a {
  1969. text-decoration: underline;
  1970. }
  1971.  
  1972. /* --- Gallery --- */
  1973. .fct-gallery-open,
  1974. body.fct-gallery-open,
  1975. body.fct-gallery-open #mainPanel {
  1976. overflow: hidden!important;
  1977. }
  1978.  
  1979. body.fct-gallery-open fullchan-x:not(.fcx-in-nav),
  1980. body.fct-gallery-open #quick-reply {
  1981. display: none!important;
  1982. }
  1983.  
  1984. fullchan-x-gallery {
  1985. position: fixed;
  1986. top: 0;
  1987. left: 0;
  1988. width: 100%;
  1989. background: rgba(0,0,0,0.9);
  1990. display: none;
  1991. height: 100%;
  1992. overflow: auto;
  1993. }
  1994.  
  1995. fullchan-x-gallery.open {
  1996. display: block;
  1997. }
  1998.  
  1999. fullchan-x-gallery .gallery {
  2000. padding: 50px 10px 0
  2001. }
  2002.  
  2003. fullchan-x-gallery .gallery__images {
  2004. --scale: 1.0;
  2005. display: flex;
  2006. width: 100%;
  2007. height: 100%;
  2008. justify-content: center;
  2009. align-content: flex-start;
  2010. gap: 4px 8px;
  2011. flex-wrap: wrap;
  2012. }
  2013.  
  2014. fullchan-x-gallery .imgLink {
  2015. float: unset;
  2016. display: block;
  2017. zoom: var(--scale);
  2018. }
  2019.  
  2020. fullchan-x-gallery .imgLink img {
  2021. border: solid white 1px;
  2022. }
  2023.  
  2024. fullchan-x-gallery .imgLink[data-filemime="video/webm"] img {
  2025. border: solid #68b723 4px;
  2026. }
  2027.  
  2028. fullchan-x-gallery .gallery__close {
  2029. border: solid 1px var(--background-color)!important;
  2030. position: fixed;
  2031. top: 60px;
  2032. right: 35px;
  2033. padding: 6px 14px;
  2034. min-height: 30px;
  2035. z-index: 10;
  2036. }
  2037.  
  2038. .fcxg .gallery__scale-options {
  2039. position: fixed;
  2040. bottom: 30px;
  2041. right: 35px;
  2042. display: flex;
  2043. gap: 14px;
  2044. z-index: 10;
  2045. }
  2046.  
  2047. .fcxg .gallery__scale-options .fullchan-x__option {
  2048. border: solid 1px var(--background-color)!important;
  2049. width: 35px;
  2050. height: 35px;
  2051. font-size: 18px;
  2052. display: flex;
  2053. justify-content: center;
  2054. }
  2055.  
  2056. .gallery__main-image {
  2057. display: none;
  2058. position: fixed;
  2059. top: 0;
  2060. left: 0;
  2061. width: 100%;
  2062. height: 100%;
  2063. justify-content: center;
  2064. align-content: center;
  2065. background: rgba(0,0,0,0.5);
  2066. }
  2067.  
  2068. .gallery__main-image img {
  2069. padding: 40px 10px 15px;
  2070. height: auto;
  2071. max-width: calc(100% - 20px);
  2072. object-fit: contain;
  2073. }
  2074.  
  2075. .gallery__main-image.active {
  2076. display: flex;
  2077. }
  2078.  
  2079. /*-- Truncated file extentions --*/
  2080. .originalNameLink[data-file-ext] {
  2081. display: inline-block;
  2082. overflow: hidden;
  2083. white-space: nowrap;
  2084. text-overflow: ellipsis;
  2085. max-width: 65px;
  2086. }
  2087.  
  2088. .originalNameLink[data-file-ext]:hover {
  2089. max-width: unset;
  2090. white-space: normal;
  2091. display: inline;
  2092. }
  2093.  
  2094. a[data-file-ext]:hover:after {
  2095. content: attr(data-file-ext);
  2096. }
  2097.  
  2098. a[data-file-ext] + .file-ext {
  2099. pointer-events: none;
  2100. }
  2101.  
  2102. a[data-file-ext]:hover + .file-ext {
  2103. display: none;
  2104. }
  2105.  
  2106. /*-- Enhanced replies --*/
  2107. .fcx-replies-plus .panelBacklinks a.fcx-active {
  2108. --active-color: red;
  2109. color: var(--active-color);
  2110. }
  2111.  
  2112.  
  2113. body:not(.fcx-icon-replies).fcx-replies-plus .panelBacklinks a.fcx-active,
  2114. .fcx-replies-plus .replyPreview .linkQuote.fcx-active-color {
  2115. background: var(--active-color);
  2116. padding: 2px 4px 2px 3px;
  2117. color: white!important;
  2118. solid 1px var(--navbar-text-color);
  2119. box-shadow: inset 0px 0px 0px 1px rgba(0,0,0,0.2);
  2120. text-shadow: 0.5px 0.5px 0.5px #000,-0.5px 0.5px 0.5px #000,-0.5px -0.5px 0.5px #000,0.5px -0.5px 0.5px #000, 0px 0px 4px #000, 0px 0px 4px #000;
  2121. background-image: linear-gradient(-45deg, rgba(0,0,0,0.2), rgba(255,255,255,0.2));
  2122. }
  2123.  
  2124. .fcx-replies-plus .replyPreview {
  2125. padding-left: 40px;
  2126. padding-right: 10px;
  2127. margin-top: 10px;
  2128. }
  2129.  
  2130. .fcx-replies-plus .altBacklinks {
  2131. background-color: unset;
  2132. }
  2133.  
  2134. .fcx-replies-plus .altBacklinks + .replyPreview {
  2135. padding-left: 4px;
  2136. padding-right: 0px;
  2137. margin-top: 5px;
  2138. }
  2139.  
  2140. .fcx-replies-plus .replyPreview .inlineQuote + .inlineQuote {
  2141. margin-top: 8px;
  2142. }
  2143.  
  2144. .fcx-replies-plus .inlineQuote .innerPost {
  2145. border: solid 1px var(--navbar-text-color)
  2146. }
  2147.  
  2148. .fcx-replies-plus .quoteLink + .inlineQuote {
  2149. margin-top: 6px;
  2150. }
  2151.  
  2152. .fcx-replies-plus .inlineQuote .postInfo > a:first-child {
  2153. position: absolute;
  2154. display: inline-block;
  2155. font-size: 0;
  2156. width: 14px;
  2157. height: 14px;
  2158. background: var(--link-color);
  2159. border-radius: 50%;
  2160. translate: 6px 0.5px;
  2161. }
  2162.  
  2163. .fcx-replies-plus .inlineQuote .postInfo > a:first-child:after {
  2164. content: '+';
  2165. display: block;
  2166. position: absolute;
  2167. left: 50%;
  2168. top: 50%;
  2169. font-size: 18px;
  2170. color: var(--contrast-color);
  2171. transform: translate(-50%, -50%) rotate(45deg);
  2172. z-index: 1;
  2173. }
  2174.  
  2175. .fcx-replies-plus .inlineQuote .postInfo > a:first-child:hover {
  2176. background: var(--link-hover-color);
  2177. }
  2178.  
  2179. .fcx-replies-plus .inlineQuote .hideButton {
  2180. margin-left: 25px;
  2181. }
  2182.  
  2183. /*-- Nav Board Links --*/
  2184. .nav-boards--custom {
  2185. display: flex;
  2186. gap: 3px;
  2187. }
  2188.  
  2189. .fcx-hidden,
  2190. #navTopBoardsSpan.fcx-hidden ~ #navBoardsSpan,
  2191. #navTopBoardsSpan.fcx-hidden ~ .nav-fade,
  2192. #navTopBoardsSpan a.fcx-hidden + span {
  2193. display: none;
  2194. }
  2195.  
  2196. /*-- Anon Unique ID posts --*/
  2197. .postInfo .spanId {
  2198. position: relative;
  2199. }
  2200.  
  2201. .fcx-id-posts {
  2202. position: absolute;
  2203. top: 0;
  2204. left: 20px;
  2205. translate: 0 calc(-100% - 5px);
  2206. display: flex;
  2207. flex-direction: column;
  2208. padding: 10px;
  2209. background: var(--background-color);
  2210. border: 1px solid var(--navbar-text-color);
  2211. width: max-content;
  2212. max-width: 500px;
  2213. max-height: 500px;
  2214. overflow: auto;
  2215. z-index: 1000;
  2216. }
  2217.  
  2218. .fcx-id-posts .nestedPost {
  2219. pointer-events: none;
  2220. width: auto;
  2221. }
  2222.  
  2223. /*-- Thread sorting --*/
  2224. #divThreads.fcx-threads {
  2225. display: flex!important;
  2226. flex-wrap: wrap;
  2227. justify-content: center;
  2228. }
  2229.  
  2230. .catalogCell.shit-thread {
  2231. order: 10;
  2232. filter: sepia(0.17);
  2233. }
  2234.  
  2235. .catalogCell.shit-thread .labelPage:after {
  2236. content: " 💩";
  2237. }
  2238.  
  2239. /* Hide navbar */
  2240. .fcx-hide-navbar .navHeader {
  2241. --translateY: -100%;
  2242. translate: 0 var(--translateY);
  2243. transition: ease 300ms translate;
  2244. }
  2245.  
  2246. .bottom-header.fcx-hide-navbar .navHeader {
  2247. --translateY: 100%;
  2248. }
  2249.  
  2250. .fcx-hide-navbar .navHeader:after {
  2251. content: "";
  2252. display: block;
  2253. height: 100%;
  2254. width: 100%;
  2255. left: 0;
  2256. position: absolute;
  2257. top: 100%;
  2258. }
  2259.  
  2260. .fcx-hide-navbar .navHeader:hover {
  2261. --translateY: -0%;
  2262. }
  2263.  
  2264. .bottom-header .fcx-hide-navbar .navHeader:not(:hover) {
  2265. --translateY: 100%;
  2266. }
  2267.  
  2268. .bottom-header .fcx-hide-navbar .navHeader:after {
  2269. top: -100%;
  2270. }
  2271.  
  2272. /* Extra styles */
  2273. .fcx-hide-delete .postInfo .deletionCheckBox {
  2274. display: none;
  2275. }
  2276.  
  2277. /*-- mascot --*/
  2278. .fcx-mascot {
  2279. position: fixed;
  2280. z-index: -1;
  2281. }
  2282.  
  2283. .fct-gallery-open .fcx-mascot {
  2284. display: none;
  2285. }
  2286.  
  2287. .fcxs-mascot-list {
  2288. display: grid;
  2289. grid-template-columns: 1fr 1fr 1fr;
  2290. gap: 10px;
  2291. margin: 25px 0 40px;
  2292. }
  2293.  
  2294. .fcxs-mascot__new,
  2295. .fcxs-mascot-card {
  2296. border: 1px solid var(--navbar-text-color);
  2297. border-radius: 8px;
  2298. position: relative;
  2299. overflow: hidden;
  2300. height: 170px;
  2301. rgba(255,255,255,0.15);
  2302. cursor: pointer;
  2303. }
  2304.  
  2305. .fcxs-mascot__new {
  2306. display: flex;
  2307. justify-content: center;
  2308. align-items: center;
  2309. font-size: 50px;
  2310. }
  2311.  
  2312. .fcxs-mascot__new span {
  2313. opacity: 0.6;
  2314. transition: ease 150ms opacity;
  2315. }
  2316.  
  2317. .fcxs-mascot__new:hover span {
  2318. opacity: 1;
  2319. }
  2320.  
  2321. .fcxs-mascot-card img {
  2322. height: 100%;
  2323. width: 100%;
  2324. object-fit: contain;
  2325. opacity: 0.7;
  2326. transition: ease 150ms all;
  2327. }
  2328.  
  2329. .fcxs-mascot-card:hover img {
  2330. opacity: 1;
  2331. }
  2332.  
  2333. .fcxs-mascot-card--disabled img {
  2334. filter: grayscale(1);
  2335. opacity: 0.4;
  2336. }
  2337.  
  2338. .fcxs-mascot-card--disabled:hover img {
  2339. filter: grayscale(0.8);
  2340. opacity: 0.6;
  2341. }
  2342.  
  2343. .fcxs-mascot-card__buttons {
  2344. border-top: solid 1px var(--navbar-text-color);
  2345. position: absolute;
  2346. bottom: 0;
  2347. left: 0;
  2348. width: 100%;
  2349. display: flex;
  2350. opacity: 0;
  2351. transition: ease 150ms opacity;
  2352. }
  2353.  
  2354. .fcxs-mascot-card:hover .fcxs-mascot-card__buttons {
  2355. opacity: 1;
  2356. }
  2357.  
  2358. .fcxs-mascot-card button {
  2359. --background-opacity: 0.5;
  2360. transition: ease 150ms all;
  2361. flex: 1;
  2362. margin: 0;
  2363. border: none;
  2364. padding: 6px 0;
  2365. color: var(--link-color);
  2366. background: rgba(255,255,255,var(--background-opacity));
  2367. }
  2368.  
  2369. .fcxs-mascot-card button + button {
  2370. border-left: solid 1px var(--navbar-text-color);
  2371. }
  2372.  
  2373. .fcxs-mascot-card button:hover {
  2374. --background-opacity: 1;
  2375. }
  2376.  
  2377. .fcxs-mascot-card__name {
  2378. position: absolute;
  2379. top: 0;
  2380. left: 0;
  2381. width: 100%;
  2382. background: rgba(255,255,255,0.2);
  2383. transition: ease 150ms background;
  2384. }
  2385.  
  2386. .fcxs-mascot-card__name:hover {
  2387. background: rgba(255,255,255,0.6);
  2388. }
  2389.  
  2390. .fcxs-mascot-card__name span {
  2391. display: block;
  2392. width: auto;
  2393. text-align: center;
  2394. white-space: nowrap;
  2395. overflow: hidden;
  2396. text-overflow: ellipsis;
  2397. padding: 2px 10px;
  2398. }
  2399.  
  2400. .fcxs-mascot-card:hover span {
  2401. white-space: normal;
  2402. overflow: hidden;
  2403. display: -webkit-box;
  2404. -webkit-line-clamp: 3;
  2405. -webkit-box-orient: vertical;
  2406. text-overflow: ellipsis;
  2407. max-height: 54px;
  2408. padding: 2px 0;
  2409. }
  2410.  
  2411. .fcxs-add-mascot {
  2412. display: none;
  2413. position: fixed;
  2414. top: 50%;
  2415. left: 50%;
  2416. translate: -50% -50%;
  2417. width: 390px;
  2418. padding: 20px;
  2419. background: var(--background-color);
  2420. border: solid 1px var(--navbar-text-color);
  2421. border-radius: 6px;
  2422. z-index: 1001;
  2423. }
  2424.  
  2425. .fcxs-close-mascot {
  2426. margin-left: auto;
  2427. }
  2428.  
  2429. .fcxs--mascot-modal .fcxs,
  2430. .fcxs--mascot-modal .fcx-settings__settings{
  2431. overflow: hidden;
  2432. }
  2433.  
  2434. .fcxs--mascot-modal .fcxs-add-mascot {
  2435. display: block;
  2436. }
  2437.  
  2438. .fcxs--mascot-modal .fcxs:after {
  2439. content: "";
  2440. display: block;
  2441. position: fixed;
  2442. top: 0;
  2443. left: 0;
  2444. width: 100%;
  2445. height: 1000vh;
  2446. background: rgba(0,0,0,0.5);
  2447. z-index: 3;
  2448. }
  2449.  
  2450. .fcxs-add-mascot-settings {
  2451. display: flex;
  2452. flex-wrap: wrap;
  2453. gap: 0 30px;
  2454. }
  2455.  
  2456. .fcxs-add-mascot-settings .fcx-setting {
  2457. min-width: 40%;
  2458. flex: 1;
  2459. }
  2460.  
  2461. .fcxs-add-mascot-settings .fcx-setting input {
  2462. width: 40px;
  2463. min-width: unset;
  2464. }
  2465.  
  2466. .fcxs-add-mascot-settings .fcx-setting--enabled,
  2467. .fcxs-add-mascot-settings .fcx-setting--name,
  2468. .fcxs-add-mascot-settings .fcx-setting--image,
  2469. .fcxs-add-mascot-settings .fcx-setting--flipImage {
  2470. max-width: 100%;
  2471. width: 100%;
  2472. flex: unset;
  2473. }
  2474.  
  2475. .fcxs-add-mascot-settings .fcx-setting--name input,
  2476. .fcxs-add-mascot-settings .fcx-setting--image input {
  2477. width: 62%;
  2478. }
  2479.  
  2480. .fcxs-add-mascot-settings .fcx-setting--enabled {
  2481. border: none;
  2482. }
  2483.  
  2484. .fcxs-add-mascot-settings .fcx-setting--id {
  2485. display: none;
  2486. }
  2487.  
  2488. .fcxs-save-mascot {
  2489. margin: 20px auto 0;
  2490. padding-left: 80px;
  2491. padding-right: 80px;
  2492. }
  2493.  
  2494. .fcxs-save-mascot[disabled] {
  2495. cursor: not-allowed;
  2496. opacity: 0.4;
  2497. }
  2498.  
  2499. .fcx-add-mascot-button .uploadCell .sizeLabel {
  2500. pointer-events: all;
  2501. position: relative;
  2502. z-index: 0;
  2503. cursor: pointer;
  2504. }
  2505.  
  2506. .fcx-add-mascot-button .uploadCell:hover .sizeLabel {
  2507. z-index: 1;
  2508. }
  2509.  
  2510. .fcx-add-mascot-button .uploadCell .sizeLabel:after {
  2511. content: "+mascot";
  2512. display: block;
  2513. position: absolute;
  2514. top: 50%;
  2515. left: 0;
  2516. transform: translateY(-50%);
  2517. width: 100%;
  2518. padding: 1px 0;
  2519. text-align: center;
  2520. border-radius: 3px;
  2521. background: var(--contrast-color);
  2522. border: 1px solid var(--text-color);
  2523. cursor: pointer;
  2524. opacity: 0;
  2525. transition: ease 150ms opacity;
  2526. }
  2527.  
  2528. .fcx-add-mascot-button .uploadCell .sizeLabel.mascotAdded:after {
  2529. content: "added!"
  2530. }
  2531.  
  2532. .fcx-add-mascot-button .uploadCell:hover .sizeLabel:after {
  2533. opacity: 1;
  2534. }
  2535.  
  2536. .fcx-add-mascot-button .quoteTooltip {
  2537. z-index: 3;
  2538. }
  2539.  
  2540. .extraMenuButton .floatingList,
  2541. .postInfo .floatingList {
  2542. z-index: 2;
  2543. }
  2544.  
  2545. /*-- Backlink icons --*/
  2546. .fcx-icon-replies .panelBacklinks > a {
  2547. font-size: 0;
  2548. text-decoration: none;
  2549. margin-left: -4px;
  2550. display: inline-block;
  2551. padding: 1px 3px;
  2552. }
  2553.  
  2554. .fcx-icon-replies .panelBacklinks > a:first-child {
  2555. margin-left: 3px;
  2556. }
  2557.  
  2558. .fcx-icon-replies .panelBacklinks > a:after {
  2559. display: inline-block;
  2560. content: '▶';
  2561. font-size: 10pt;
  2562. rotate: 0deg;
  2563. transition: ease 75ms all;
  2564. }
  2565.  
  2566. .fcx-icon-replies .opCell .panelBacklinks > a.fcx-active:after {
  2567. rotate: 90deg;
  2568. text-shadow: 0px 1px 0px #000, 1.8px 0px 0px #000, -0.8px -1.5px 0px #000, -0.8px 1.5px 0px #000;
  2569. }
  2570.  
  2571. /*-- 8chan jank CSS fix --*/
  2572. .spoiler > .inlineQuote {
  2573. color: initial!important;
  2574. }
  2575. .spoiler > .inlineQuote .quoteLink {
  2576. color: #ff0000!important;
  2577. }
  2578.  
  2579. .spoiler > .inlineQuote .greenText {
  2580. color: #429406!important;
  2581. }
  2582.  
  2583. .spoiler > .inlineQuote .spoiler {
  2584. color: #000!important;
  2585. }
  2586.  
  2587. #helpMenu {
  2588. display: none;
  2589. }
  2590.  
  2591. @media only screen and (min-width: 813px) {
  2592. .fcxm {
  2593. position: fixed;
  2594. display: flex;
  2595. flex-direction: column;
  2596. top: 25px;
  2597. left: 0;
  2598. width: 100%;
  2599. min-height: 300px;
  2600. max-height: 80vh;
  2601. background: var(--contrast-color);
  2602. border-bottom: 1px solid var(--horizon-sep-color);
  2603. box-shadow: 0 5px 10px rgba(0,0,0,.5);
  2604. translate: 0 -110%;
  2605. transition: ease 150ms translate
  2606. }
  2607.  
  2608. .fcx-active > .fcxm {
  2609. translate: 0 0%;
  2610. }
  2611.  
  2612. .fcxm__menu-tabs {
  2613. display: flex;
  2614. justify-content: center;
  2615. margin-top: 20px;
  2616. padding: 0 15px;
  2617. border-bottom: 1px solid var(--horizon-sep-color);
  2618. }
  2619.  
  2620. .fcxm__menu-button {
  2621. margin: 0;
  2622. padding: 10px;
  2623. min-width: 120px;
  2624. border: 1px solid var(--horizon-sep-color);
  2625. border-width: 1px 0 0 1px;
  2626. }
  2627.  
  2628. .fcxm__menu-button.fcx-active {
  2629. background: var(--horizon-sep-color);
  2630. }
  2631.  
  2632. .fcxm__menu-button:last-child {
  2633. border-width: 1px 1px 0 1px;
  2634. }
  2635.  
  2636. .fcxm__menu-container {
  2637. flex: 1;
  2638. position: relative;
  2639. padding: 0 15px 25px;
  2640. overflow: auto;
  2641. }
  2642.  
  2643. #fcxm .floatingMenu {
  2644. display: none!important;
  2645. box-shadow: none!important;
  2646. position: static;
  2647. background: none;
  2648. border: none;
  2649. height: 100%;
  2650. width: 100%;
  2651. padding: 0!important;
  2652. }
  2653.  
  2654. #fcxm .floatingMenu.fcx-active {
  2655. display: block!important;
  2656. min-height: 100%;
  2657. }
  2658.  
  2659. .fcxm .floatingMenu .handle,
  2660. .fcxm .floatingMenu .handle + hr {
  2661. display: none;
  2662. }
  2663.  
  2664. .fcxm .floatingContainer {
  2665. resize: unset;
  2666. height: 100%!important;
  2667. width: 100%!important;
  2668. background: rgba(0,0,0,0);
  2669. }
  2670.  
  2671. .fcxm #settingsMenu .settingsTab {
  2672. text-align: center;
  2673. color: var(--text-color);
  2674. text-shadow: none;
  2675. padding-top: 10px;
  2676. }
  2677.  
  2678. .fcxm #settingsMenu .floatingContainer > div:has(.settingsTab),
  2679. .fcxm #settingsMenu .menuContentPanel {
  2680. display: grid;
  2681. grid-template-columns: repeat(4, 1fr);
  2682. direction: rtl;
  2683. }
  2684.  
  2685. .fcxm #settingsMenu .menuContentPanel > * {
  2686. direction: ltr;
  2687. }
  2688.  
  2689. .fcxm #settingsMenu .panelContents {
  2690. display: block;
  2691. }
  2692.  
  2693. .fcxm #settingsMenu .panelContents:last-child > div {
  2694. display: flex;
  2695. align-items: center;
  2696. gap: 10px;
  2697. }
  2698.  
  2699. .fcxm #settingsMenu .panelContents:last-child > div:has(input[type="range"]) {
  2700. line-height: 2;
  2701. }
  2702.  
  2703. .fcxm #settingsMenu .panelContents:last-child > div select {
  2704. font-size: 12px;
  2705. }
  2706.  
  2707. /*-- Help menu --*/
  2708. .fcx-header-menu #helpMenu {
  2709. height: 100%;
  2710. }
  2711.  
  2712. .fcxm__help-page-content {
  2713. width: 100%;
  2714. display: grid;
  2715. grid-auto-columns: 1fr 1fr;
  2716. margin: 0;
  2717. gap: 12px;
  2718. max-width: 1280px;
  2719. margin: auto;
  2720. padding-top: 20px;
  2721. }
  2722.  
  2723. .fcxm__help-page-content .titleFieldset {
  2724. width: auto;
  2725. grid-column-start: 2;
  2726. margin: 0;
  2727. border: none;
  2728. font-size: 12px;
  2729. }
  2730.  
  2731. .fcxm__help-page-content .titleFieldset:nth-child(4) {
  2732. grid-column-start: 1;
  2733. grid-row-start: 1;
  2734. grid-row-end: 5;
  2735. }
  2736.  
  2737. .fcxm__help-page-content .titleFieldset legend {
  2738. font-size: 14px;
  2739. }
  2740.  
  2741. /*-- watch list columns --*/
  2742. #fcxm #watchedMenu .floatingContainer,
  2743. #fcxm #multiboardMenu .floatingContainer{
  2744. overflow-x: auto;
  2745. display: flex;
  2746. width: 100%;
  2747. max-width: unset;
  2748. justify-content: center;
  2749. }
  2750.  
  2751. .fcx-header-menu #multiboardMenu .floatingContainer {
  2752. padding-top: 20px;
  2753. }
  2754.  
  2755. .fcx-header-menu #watchedMenu table,
  2756. .fcx-header-menu #multiboardMenu table {
  2757. display: flex;
  2758. flex-direction: column;
  2759. height: 100%;
  2760. flex-wrap: wrap;
  2761. max-height: 450px;
  2762. gap: 0 20px;
  2763. align-content: center;
  2764. padding-top: 20px;
  2765. }
  2766.  
  2767. .fcx-header-menu #multiboardMenu table {
  2768. max-height: 350px;
  2769. }
  2770.  
  2771. .fcx-header-menu #watchedMenu table > tr,
  2772. .fcx-header-menu #multiboardMenu table > tr {
  2773. width: unset;
  2774. max-width: 500px;
  2775. }
  2776.  
  2777. .fcx-header-menu #watchedMenu .watchedCellLabel,
  2778. .fcx-header-menu #multiboardMenu td.mb-cell-string {
  2779. padding-right: 40px;
  2780. }
  2781.  
  2782. .fcx-header-menu .watchedCellLabel a::before {
  2783. color: var(--role-color);
  2784. }
  2785.  
  2786. .fcx-header-menu .watchedCellLabel:has(.watchedNotification:not(:empty)) a {
  2787. color: var(--error);
  2788. }
  2789.  
  2790. .fcx-header-menu .settingsButton {
  2791. display: inline-block;
  2792. width: 12px;
  2793. height: 100%;
  2794. }
  2795.  
  2796. .fcx-menu-trigger {
  2797. position: absolute;
  2798. left: 102px;
  2799. top: 1px;
  2800. height: calc(100% - 1px);
  2801. width: 110px;
  2802. z-index: 3000;
  2803. border-radius: 6px;
  2804. }
  2805.  
  2806. .fcx-menu-trigger.fcx-active {
  2807. box-shadow: inset 0px 0px 5px 0px var(--link-color);
  2808. }
  2809. }
  2810. `;
  2811.  
  2812.  
  2813. document.head.appendChild(style);
  2814. document.body.appendChild(fcxs);
  2815. fcxs.init();
  2816.  
  2817. // Asuka and Eris (fantasy Asuka) are best girls
  2818.  
Add Comment
Please, Sign In to add comment