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