Fullchan X

8chan features script

当前为 2025-04-29 提交的版本,查看 最新版本

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