Greasy Fork is available in English.

Fullchan X

8chan features script

安裝腳本?
作者推薦腳本

您可能也會喜歡 8chanSS

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