Greasy Fork 支持 简体中文。

Fullchan X

8chan features script

目前為 2025-04-30 提交的版本,檢視 最新版本

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