Fullchan X

8chan features script

目前为 2025-04-20 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Fullchan X
  3. // @namespace Violentmonkey Scripts
  4. // @match https://8chan.moe/*/res/*
  5. // @match https://8chan.se/*/res/*
  6. // @match https://8chan.moe/*/catalog*
  7. // @match https://8chan.se/*/catalog*
  8. // @run-at document-idle
  9. // @grant none
  10. // @version 1.6.7
  11. // @author vfyxe
  12. // @description 8chan features script
  13. // ==/UserScript==
  14.  
  15. class fullChanX extends HTMLElement {
  16. constructor() {
  17. super();
  18. this.settingsEl = document.querySelector('fullchan-x-settings');
  19. this.settings = this.settingsEl.settings;
  20. this.isThread = !!document.querySelector('.opCell');
  21. this.isDisclaimer = window.location.href.includes('disclaimer');
  22. Object.keys(this.settings).forEach(key => {
  23. this[key] = this.settings[key]?.value;
  24. });
  25. }
  26.  
  27. init() {
  28. this.settingsButton = this.querySelector('#fcx-settings-btn');
  29. this.settingsButton.addEventListener('click', () => this.settingsEl.toggle());
  30. this.handleBoardLinks();
  31. if (!this.isThread) return;
  32.  
  33. this.quickReply = document.querySelector('#quick-reply');
  34. this.qrbody = document.querySelector('#qrbody');
  35. this.threadParent = document.querySelector('#divThreads');
  36. this.threadId = this.threadParent.querySelector('.opCell').id;
  37. this.thread = this.threadParent.querySelector('.divPosts');
  38. this.posts = [...this.thread.querySelectorAll('.postCell')];
  39. this.postOrder = 'default';
  40. this.postOrderSelect = this.querySelector('#thread-sort');
  41. this.myYousLabel = this.querySelector('.my-yous__label');
  42. this.yousContainer = this.querySelector('#my-yous');
  43.  
  44. this.gallery = document.querySelector('fullchan-x-gallery');
  45. this.galleryButton = this.querySelector('#fcx-gallery-btn');
  46.  
  47. this.updateYous();
  48. this.observers();
  49.  
  50. if (this.enableFileExtentions) this.handleTruncatedFilenames();
  51. }
  52.  
  53. styleUI () {
  54. this.style.setProperty('--top', this.uiTopPosition);
  55. this.style.setProperty('--right', this.uiRightPosition);
  56. this.classList.toggle('fcx--dim', this.uiDimWhenInactive);
  57. this.classList.toggle('page-thread', this.isThread);
  58. const style = document.createElement('style');
  59. if (this.hideDefaultBoards !== '') {
  60. style.textContent += '#navTopBoardsSpan{display:block!important;}'
  61. }
  62. document.body.appendChild(style);
  63. }
  64.  
  65. handleBoardLinks () {
  66. const navBoards = document.querySelector('#navTopBoardsSpan');
  67. const customBoardLinks = this.customBoardLinks?.toLowerCase().replace(/\s/g,'').split(',');
  68. let hideDefaultBoards = this.hideDefaultBoards?.toLowerCase().replace(/\s/g,'') || '';
  69. const urlCatalog = this.catalogBoardLinks ? '/catalog.html' : '';
  70.  
  71.  
  72. if (hideDefaultBoards === 'all') {
  73. navBoards.classList.add('hidden');
  74. } else {
  75. const waitForNavBoards = setInterval(() => {
  76. const navBoards = document.querySelector('#navTopBoardsSpan');
  77. if (!navBoards || !navBoards.querySelector('a')) return;
  78.  
  79. clearInterval(waitForNavBoards);
  80.  
  81. hideDefaultBoards = hideDefaultBoards.split(',');
  82. const defaultLinks = [...navBoards.querySelectorAll('a')];
  83. defaultLinks.forEach(link => {
  84. link.href += urlCatalog;
  85. const linkText = link.textContent;
  86. const shouldHide = hideDefaultBoards.includes(linkText) || customBoardLinks.includes(linkText);
  87. link.classList.toggle('hidden', shouldHide);
  88. });
  89. }, 50);
  90. }
  91.  
  92. if (this.customBoardLinks.length > 0) {
  93. const customNav = document.createElement('span');
  94. customNav.classList = 'nav-boards nav-boards--custom';
  95. customNav.innerHTML = '<span>[</span>';
  96.  
  97. customBoardLinks.forEach((board, index) => {
  98. const link = document.createElement('a');
  99. link.href = '/' + board + urlCatalog;
  100. link.textContent = board;
  101. customNav.appendChild(link);
  102. if (index < customBoardLinks.length - 1) customNav.innerHTML += '<span>/</span>';
  103. });
  104.  
  105. customNav.innerHTML += '<span>]</span>';
  106. navBoards.parentNode.insertBefore(customNav, navBoards);
  107. }
  108. }
  109.  
  110. observers () {
  111. this.postOrderSelect.addEventListener('change', (event) => {
  112. this.postOrder = event.target.value;
  113. this.assignPostOrder();
  114. });
  115.  
  116. const observerCallback = (mutationsList, observer) => {
  117. for (const mutation of mutationsList) {
  118. if (mutation.type === 'childList') {
  119. this.posts = [...this.thread.querySelectorAll('.postCell')];
  120. if (this.postOrder !== 'default') this.assignPostOrder();
  121. this.updateYous();
  122. this.gallery.updateGalleryImages();
  123. if (this.settings.enableFileExtentions) this.handleTruncatedFilenames();
  124. }
  125. }
  126. };
  127.  
  128. const threadObserver = new MutationObserver(observerCallback);
  129. threadObserver.observe(this.thread, { childList: true, subtree: false });
  130.  
  131. if (this.enableNestedQuotes) {
  132. this.thread.addEventListener('click', event => {
  133. this.handleClick(event);
  134. });
  135. }
  136.  
  137. this.galleryButton.addEventListener('click', () => this.gallery.open());
  138. }
  139.  
  140. handleClick (event) {
  141. const clicked = event.target;
  142.  
  143. const post = clicked.closest('.innerPost') || clicked.closest('.innerOP');
  144. if (!post) return;
  145.  
  146. const isNested = !!post.closest('.innerNested');
  147. const nestQuote = clicked.closest('.quoteLink');
  148. const postMedia = clicked.closest('a[data-filemime]');
  149. const postId = clicked.closest('.linkQuote');
  150.  
  151. if (nestQuote) {
  152. event.preventDefault();
  153. this.nestQuote(nestQuote);
  154. } else if (postMedia && isNested) {
  155. this.handleMediaClick(event, postMedia);
  156. } else if (postId && isNested) {
  157. this.handleIdClick(postId);
  158. }
  159. }
  160.  
  161. handleMediaClick (event, postMedia) {
  162. if (postMedia.dataset.filemime === "video/webm") return;
  163. event.preventDefault();
  164. const imageSrc = `${postMedia.href}`;
  165. const imageEl = postMedia.querySelector('img');
  166. if (!postMedia.dataset.thumbSrc) postMedia.dataset.thumbSrc = `${imageEl.src}`;
  167.  
  168. const isExpanding = imageEl.src !== imageSrc;
  169.  
  170. if (isExpanding) {
  171. imageEl.src = imageSrc;
  172. imageEl.classList
  173. }
  174. imageEl.src = isExpanding ? imageSrc : postMedia.dataset.thumbSrc;
  175. imageEl.classList.toggle('imgExpanded', isExpanding);
  176. }
  177.  
  178. handleIdClick (postId) {
  179. const idNumber = '>>' + postId.textContent;
  180. this.quickReply.style.display = 'block';
  181. this.qrbody.value += idNumber + '\n';
  182. }
  183.  
  184. handleTruncatedFilenames () {
  185. this.postFileNames = [...this.threadParent.querySelectorAll('.originalNameLink[download]:not([data-file-ext])')];
  186. this.postFileNames.forEach(fileName => {
  187. const strings = fileName.textContent.split('.');
  188. fileName.textContent = strings[0];
  189. fileName.dataset.fileExt = `.${strings[1]}`;
  190. const typeEl = document.createElement('a');
  191. typeEl.textContent = `.${strings[1]}`;
  192. typeEl.classList = ('file-ext originalNameLink');
  193. fileName.parentNode.insertBefore(typeEl, fileName.nextSibling);
  194. });
  195. }
  196.  
  197. assignPostOrder () {
  198. const postOrderReplies = (post) => {
  199. const replyCount = post.querySelectorAll('.panelBacklinks a').length;
  200. post.style.order = 100 - replyCount;
  201. }
  202.  
  203. const postOrderCatbox = (post) => {
  204. const postContent = post.querySelector('.divMessage').textContent;
  205. const matches = postContent.match(/catbox\.moe/g);
  206. const catboxCount = matches ? matches.length : 0;
  207. post.style.order = 100 - catboxCount;
  208. }
  209.  
  210. if (this.postOrder === 'default') {
  211. this.thread.style.display = 'block';
  212. return;
  213. }
  214.  
  215. this.thread.style.display = 'flex';
  216.  
  217. if (this.postOrder === 'replies') {
  218. this.posts.forEach(post => postOrderReplies(post));
  219. } else if (this.postOrder === 'catbox') {
  220. this.posts.forEach(post => postOrderCatbox(post));
  221. }
  222. }
  223.  
  224. updateYous () {
  225. this.yous = this.posts.filter(post => post.querySelector('.quoteLink.you'));
  226. this.yousLinks = this.yous.map(you => {
  227. const youLink = document.createElement('a');
  228. youLink.textContent = '>>' + you.id;
  229. youLink.href = '#' + you.id;
  230. return youLink;
  231. })
  232.  
  233. let hasUnseenYous = false;
  234. this.setUnseenYous();
  235.  
  236. this.yousContainer.innerHTML = '';
  237. this.yousLinks.forEach(you => {
  238. const youId = you.textContent.replace('>>', '');
  239. if (!this.seenYous.includes(youId)) {
  240. you.classList.add('unseen');
  241. hasUnseenYous = true
  242. }
  243. this.yousContainer.appendChild(you)
  244. });
  245.  
  246. this.myYousLabel.classList.toggle('unseen', hasUnseenYous);
  247.  
  248. if (this.replyTabIcon === '') return;
  249. const icon = this.replyTabIcon;
  250. document.title = hasUnseenYous
  251. ? document.title.startsWith(`${icon} `)
  252. ? document.title
  253. : `${icon} ${document.title}`
  254. : document.title.replace(new RegExp(`^${icon} `), '');
  255. }
  256.  
  257. observeUnseenYou(you) {
  258. you.classList.add('observe-you');
  259.  
  260. const observer = new IntersectionObserver((entries, observer) => {
  261. entries.forEach(entry => {
  262. if (entry.isIntersecting) {
  263. const id = you.id;
  264. you.classList.remove('observe-you');
  265.  
  266. if (!this.seenYous.includes(id)) {
  267. this.seenYous.push(id);
  268. localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous));
  269. }
  270.  
  271. observer.unobserve(you);
  272. this.updateYous();
  273.  
  274. }
  275. });
  276. }, { rootMargin: '0px', threshold: 0.1 });
  277.  
  278. observer.observe(you);
  279. }
  280.  
  281. setUnseenYous() {
  282. this.seenKey = `${this.threadId}-seen-yous`;
  283. this.seenYous = JSON.parse(localStorage.getItem(this.seenKey));
  284.  
  285. if (!this.seenYous) {
  286. this.seenYous = [];
  287. localStorage.setItem(this.seenKey, JSON.stringify(this.seenYous));
  288. }
  289.  
  290. this.unseenYous = this.yous.filter(you => !this.seenYous.includes(you.id));
  291.  
  292. this.unseenYous.forEach(you => {
  293. if (!you.classList.contains('observe-you')) {
  294. this.observeUnseenYou(you);
  295. }
  296. });
  297. }
  298.  
  299. nestQuote(quoteLink) {
  300. const parentPostMessage = quoteLink.closest('.divMessage');
  301. const quoteId = quoteLink.href.split('#')[1];
  302. const quotePost = document.getElementById(quoteId);
  303. if (!quotePost) return;
  304.  
  305. const quotePostContent = quotePost.querySelector('.innerOP') || quotePost.querySelector('.innerPost');
  306. if (!quotePostContent) return;
  307.  
  308. const existing = parentPostMessage.querySelector(`.nestedPost[data-quote-id="${quoteId}"]`);
  309. if (existing) {
  310. existing.remove();
  311. return;
  312. }
  313.  
  314. const wrapper = document.createElement('div');
  315. wrapper.classList.add('nestedPost');
  316. wrapper.setAttribute('data-quote-id', quoteId);
  317.  
  318. const clone = quotePostContent.cloneNode(true);
  319. clone.style.whiteSpace = 'unset';
  320. clone.classList.add('innerNested');
  321. wrapper.appendChild(clone);
  322.  
  323. parentPostMessage.appendChild(wrapper);
  324. }
  325. };
  326.  
  327. window.customElements.define('fullchan-x', fullChanX);
  328.  
  329.  
  330. class fullChanXGallery extends HTMLElement {
  331. constructor() {
  332. super();
  333. }
  334.  
  335. init() {
  336. this.fullchanX = document.querySelector('fullchan-x');
  337. this.imageContainer = this.querySelector('.gallery__images');
  338. this.mainImageContainer = this.querySelector('.gallery__main-image');
  339. this.mainImage = this.mainImageContainer.querySelector('img');
  340. this.closeButton = this.querySelector('.gallery__close');
  341. this.listeners();
  342. this.addGalleryImages();
  343. this.initalized = true;
  344. }
  345.  
  346. addGalleryImages () {
  347. this.thumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].map(thumb => {
  348. return thumb.cloneNode(true);
  349. });
  350.  
  351. this.thumbs.forEach(thumb => {
  352. this.imageContainer.appendChild(thumb);
  353. });
  354. }
  355.  
  356. updateGalleryImages () {
  357. if (!this.initalized) return;
  358.  
  359. const newThumbs = [...this.fullchanX.threadParent.querySelectorAll('.imgLink')].filter(thumb => {
  360. return !this.thumbs.find(thisThumb.href === thumb.href);
  361. }).map(thumb => {
  362. return thumb.cloneNode(true);
  363. });
  364.  
  365. newThumbs.forEach(thumb => {
  366. this.thumbs.push(thumb);
  367. this.imageContainer.appendChild(thumb);
  368. });
  369. }
  370.  
  371. listeners () {
  372. this.addEventListener('click', event => {
  373. const clicked = event.target;
  374.  
  375. let imgLink = clicked.closest('.imgLink');
  376. if (imgLink?.dataset.filemime === 'video/webm') return;
  377.  
  378. if (imgLink) {
  379. event.preventDefault();
  380. this.mainImage.src = imgLink.href;
  381. }
  382.  
  383.  
  384. this.mainImageContainer.classList.toggle('active', !!imgLink);
  385.  
  386. if (clicked.closest('.gallery__close')) this.close();
  387. });
  388. }
  389.  
  390. open () {
  391. if (!this.initalized) this.init();
  392. this.classList.add('open');
  393. document.body.classList.add('fct-gallery-open');
  394. }
  395.  
  396. close () {
  397. this.classList.remove('open');
  398. document.body.classList.remove('fct-gallery-open');
  399. }
  400. }
  401.  
  402. window.customElements.define('fullchan-x-gallery', fullChanXGallery);
  403.  
  404.  
  405.  
  406. class fullChanXSettings extends HTMLElement {
  407. constructor() {
  408. super();
  409. this.settingsKey = 'fullchan-x-settings';
  410. this.inputs = [];
  411. this.settings = {};
  412. this.settingsTemplate = {
  413. enableNestedQuotes: {
  414. info: 'Nest posts when clicking backlinks.',
  415. type: 'checkbox',
  416. value: true
  417. },
  418. enableFileExtentions: {
  419. info: 'Always show filetype on shortened file names.',
  420. type: 'checkbox',
  421. value: true
  422. },
  423. customBoardLinks: {
  424. info: 'List of custom boards in nav (seperate by comma)',
  425. type: 'input',
  426. value: 'v,a,b'
  427. },
  428. hideDefaultBoards: {
  429. info: 'List of boards to remove from nav (seperate by comma). Set as "all" to remove all.',
  430. type: 'input',
  431. value: 'interracial,mlp'
  432. },
  433. catalogBoardLinks: {
  434. info: 'Redirect nav board links to catalog pages.',
  435. type: 'checkbox',
  436. value: true
  437. },
  438. uiTopPosition: {
  439. info: 'Position from top of screen e.g. 100px',
  440. type: 'input',
  441. value: '50px'
  442. },
  443. uiRightPosition: {
  444. info: 'Position from right of screen e.g. 100px',
  445. type: 'input',
  446. value: '25px'
  447. },
  448. uiDimWhenInactive: {
  449. info: 'Dim UI when not hovering with mouse.',
  450. type: 'checkbox',
  451. value: true
  452. },
  453. replyTabIcon: {
  454. info: 'Set the icon/text added to tab title when you get a new (You).',
  455. type: 'input',
  456. value: '❗'
  457. },
  458. };
  459. }
  460.  
  461. init() {
  462. this.settingsContainer = this.querySelector('.fcx-settings__settings');
  463. this.getSavedSettings();
  464. this.buildSettingsOptions();
  465. this.listeners();
  466. this.querySelector('.fcx-settings__close').addEventListener('click', () => this.close());
  467. }
  468.  
  469. setSavedSettings (updated) {
  470. localStorage.setItem(this.settingsKey, JSON.stringify(this.settings));
  471. if (updated) this.classList.add('fcxs-updated');
  472. }
  473.  
  474. getSavedSettings() {
  475. const saved = JSON.parse(localStorage.getItem(this.settingsKey));
  476. if (saved) this.settings = saved;
  477. }
  478.  
  479. listeners() {
  480. this.inputs.forEach(input => {
  481. input.addEventListener('change', () => {
  482. const key = input.name;
  483. const value = input.type === 'checkbox' ? input.checked : input.value;
  484. this.settings[key].value = value;
  485. this.setSavedSettings(true);
  486. });
  487. });
  488. }
  489.  
  490. buildSettingsOptions() {
  491. Object.entries(this.settingsTemplate).forEach(([key, config]) => {
  492. const wrapper = document.createElement('div');
  493. const infoWrapper = document.createElement('div');
  494. wrapper.classList.add('fcx-setting');
  495. infoWrapper.classList.add('fcx-setting__info');
  496. wrapper.appendChild(infoWrapper);
  497.  
  498. const label = document.createElement('label');
  499. label.textContent = key
  500. .replace(/([A-Z])/g, ' $1')
  501. .replace(/^./, str => str.toUpperCase());
  502. label.setAttribute('for', key);
  503. infoWrapper.appendChild(label);
  504.  
  505. if (config.info) {
  506. const info = document.createElement('p');
  507. info.textContent = config.info;
  508. infoWrapper.appendChild(info);
  509. }
  510.  
  511. const savedValue = this.settings[key]?.value ?? config.value;
  512.  
  513. let input;
  514.  
  515. if (config.type === 'checkbox') {
  516. input = document.createElement('input');
  517. input.type = 'checkbox';
  518. input.checked = savedValue;
  519. } else if (config.type === 'input') {
  520. input = document.createElement('input');
  521. input.type = 'text';
  522. input.value = savedValue;
  523. } else if (config.type === 'select') {
  524. input = document.createElement('select');
  525. const options = config.options.split(',');
  526. options.forEach(opt => {
  527. const option = document.createElement('option');
  528. option.value = opt;
  529. option.textContent = opt;
  530. if (opt === savedValue) option.selected = true;
  531. input.appendChild(option);
  532. });
  533. }
  534.  
  535. if (input) {
  536. input.id = key;
  537. input.name = key;
  538. wrapper.appendChild(input);
  539. this.inputs.push(input);
  540. this.settings[key] = { value: input.type === 'checkbox' ? input.checked : input.value };
  541. }
  542.  
  543. this.settingsContainer.appendChild(wrapper);
  544. });
  545.  
  546. this.setSavedSettings();
  547. }
  548.  
  549. open() {
  550. this.classList.add('open');
  551. }
  552.  
  553. close() {
  554. this.classList.remove('open');
  555. }
  556.  
  557. toggle() {
  558. this.classList.toggle('open');
  559. }
  560. }
  561.  
  562. window.customElements.define('fullchan-x-settings', fullChanXSettings);
  563.  
  564.  
  565.  
  566. // Create fullchan-x settings
  567. const fcxs = document.createElement('fullchan-x-settings');
  568. fcxs.innerHTML = `
  569. <div class="fcxs fcx-settings">
  570. <header>
  571. <span class="fcx-settings__title">
  572. Fullchan-X Settings
  573. </span>
  574. <button class="fcx-settings__close fullchan-x__option">Close</button>
  575. </header>
  576.  
  577. <main>
  578. <div class="fcxs__updated-message">
  579. <p>Settings updated, refresh page to apply</p>
  580. <button class="fullchan-x__option" onClick="location.reload()">Reload page</button>
  581. </div>
  582. <div class="fcx-settings__settings"></div>
  583. </main>
  584.  
  585. <footer>
  586. </footer>
  587. </div>
  588. `;
  589. document.body.appendChild(fcxs);
  590. fcxs.init();
  591.  
  592.  
  593. // Create fullchan-x gallery
  594. const fcxg = document.createElement('fullchan-x-gallery');
  595. fcxg.innerHTML = `
  596. <div class="gallery">
  597. <button id="#fcxg-close" class="gallery__close fullchan-x__option">Close</button>
  598. <div id="#fcxg-images" class="gallery__images"></div>
  599. <div id="#fcxg-main-image" class="gallery__main-image">
  600. <img src="" />
  601. </div>
  602. </div>
  603. `;
  604. document.body.appendChild(fcxg);
  605.  
  606.  
  607.  
  608. // Create fullchan-x element
  609. const fcx = document.createElement('fullchan-x');
  610. fcx.innerHTML = `
  611. <div class="fcx__controls">
  612. <button id="fcx-settings-btn" class="fullchan-x__option fcx-settings-toggle">
  613. ⚙️<span>Settings</span>
  614. </button>
  615.  
  616. <div class="fullchan-x__option thread-only">
  617. <select id="thread-sort">
  618. <option value="default">Default</option>
  619. <option value="replies">Replies</option>
  620. <option value="catbox">Catbox</option>
  621. </select>
  622. </div>
  623.  
  624. <button id="fcx-gallery-btn" class="gallery__toggle fullchan-x__option thread-only">
  625. 🖼️<span>Gallery</span>
  626. </button>
  627.  
  628. <div class="fcx__my-yous thread-only">
  629. <p class="my-yous__label fullchan-x__option">💬<span>My (You)s</span></p>
  630. <div class="my-yous__yous" id="my-yous"></div>
  631. </div>
  632. </div>
  633. `;
  634. document.body.appendChild(fcx);
  635. fcx.styleUI()
  636. onload = (event) => fcx.init();
  637.  
  638.  
  639. // Styles
  640. const style = document.createElement('style');
  641. style.innerHTML = `
  642. fullchan-x {
  643. --top: 50px;
  644. --right: 25px;
  645. top: var(--top);
  646. right: var(--right);
  647. display: block;
  648. position: fixed;
  649. padding: 10px;
  650. background: var(--background-color);
  651. border: 1px solid var(--navbar-text-color);
  652. color: var(--link-color);
  653. font-size: 14px;
  654. z-index: 1000;
  655. }
  656.  
  657. fullchan-x:not(.page-thread) .thread-only,
  658. fullchan-x:not(.page-catalog) .catalog-only{
  659. display: none!important;
  660. }
  661.  
  662. fullchan-x:not(:hover):not(:has(select:focus)) {
  663. z-index: 3;
  664. }
  665.  
  666. fullchan-x.fcx--dim:not(:hover) {
  667. opacity: 0.6;
  668. }
  669.  
  670. .divPosts {
  671. flex-direction: column;
  672. }
  673.  
  674. .fcx__controls {
  675. display: flex;
  676. flex-direction: column;
  677. gap: 6px;
  678. }
  679.  
  680. fullchan-x:not(:hover):not(:has(select:focus)) span,
  681. fullchan-x:not(:hover):not(:has(select:focus)) select {
  682. display: none;
  683. margin-left: 5px;
  684. z-index:3;
  685. }
  686.  
  687. .fcx__controls span,
  688. .fcx__controls select {
  689. margin-left: 5px;
  690. }
  691.  
  692. #thread-sort {
  693. border: none;
  694. background: none;
  695. }
  696.  
  697. .my-yous__yous {
  698. display: none;
  699. flex-direction: column;
  700. padding-top: 10px;
  701. max-height: calc(100vh - 220px - var(--top));
  702. overflow: auto;
  703. }
  704.  
  705. .fcx__my-yous:hover .my-yous__yous {
  706. display: flex;
  707. }
  708.  
  709. .fullchan-x__option {
  710. display: flex;
  711. padding: 6px 8px;
  712. background: white;
  713. border: none !important;
  714. border-radius: 0.2rem;
  715. transition: all ease 150ms;
  716. cursor: pointer;
  717. margin: 0;
  718. text-align: left;
  719. min-width: 18px;
  720. min-height: 18px;
  721. align-items: center;
  722. }
  723.  
  724. .fullchan-x__option,
  725. .fullchan-x__option select {
  726. font-size: 12px;
  727. font-weight: 400;
  728. color: #374369;
  729. }
  730.  
  731. fullchan-x:not(:hover):not(:has(select:focus)) .fullchan-x__option {
  732. display: flex;
  733. justify-content: center;
  734. }
  735.  
  736. #thread-sort {
  737. padding-right: 0;
  738. }
  739.  
  740. #thread-sort:hover {
  741. display: block;
  742. }
  743.  
  744. .innerPost:has(.quoteLink.you) {
  745. border-left: solid #dd003e 6px;
  746. }
  747.  
  748. .innerPost:has(.youName) {
  749. border-left: solid #68b723 6px;
  750. }
  751.  
  752. /* --- Nested quotes --- */
  753. .divMessage .nestedPost {
  754. display: block;
  755. white-space: normal!important;
  756. overflow-wrap: anywhere;
  757. margin-top: 0.5em;
  758. border: 1px solid var(--navbar-text-color);
  759. }
  760.  
  761. .nestedPost .innerPost,
  762. .nestedPost .innerOP {
  763. width: 100%;
  764. }
  765.  
  766. .nestedPost .imgLink .imgExpanded {
  767. width: auto!important;
  768. height: auto!important;
  769. }
  770.  
  771. .my-yous__label.unseen {
  772. background: var(--link-hover-color);
  773. color: white;
  774. }
  775.  
  776. .my-yous__yous .unseen {
  777. font-weight: 900;
  778. color: var(--link-hover-color);
  779. }
  780.  
  781.  
  782.  
  783. /*--- Settings --- */
  784. .fcx-settings {
  785. display: block;
  786. position: fixed;
  787. top: 50vh;
  788. left: 50vw;
  789. translate: -50% -50%;
  790. padding: 20px 0;
  791. background: var(--background-color);
  792. border: 1px solid var(--navbar-text-color);
  793. color: var(--link-color);
  794. font-size: 14px;
  795. max-width: 480px;
  796. max-height: 80vh;
  797. overflow: scroll;
  798. }
  799.  
  800. fullchan-x-settings:not(.open) {
  801. display: none;
  802. }
  803.  
  804. .fcx-settings > * {
  805. padding: 0 20px;
  806. }
  807.  
  808. .fcx-settings header {
  809. display: flex;
  810. align-items: center;
  811. justify-content: space-between;
  812. margin: 0 0 15px;
  813. padding-bottom: 20px;
  814. border-bottom: 1px solid var(--navbar-text-color);
  815. }
  816.  
  817. .fcx-settings__title {
  818. font-size: 24px;
  819. font-size: 24px;
  820. letter-spacing: 0.04em;
  821. }
  822.  
  823. fullchan-x-settings:not(.fcxs-updated) .fcxs__updated-message {
  824. display: none;
  825. }
  826.  
  827. .fcx-setting {
  828. display: flex;
  829. justify-content: space-between;
  830. align-items: center;
  831. padding: 12px 0;
  832. }
  833.  
  834. .fcx-setting__info {
  835. max-width: 60%;
  836. }
  837.  
  838. .fcx-setting input[type="text"],
  839. .fcx-setting select {
  840. padding: 4px 6px;
  841. min-width: 35%;
  842. }
  843.  
  844. .fcx-setting label {
  845. font-weight: 600;
  846. }
  847.  
  848. .fcx-setting p {
  849. margin: 6px 0 0;
  850. font-size: 12px;
  851. }
  852.  
  853. .fcx-setting + .fcx-setting {
  854. border-top: 1px solid var(--navbar-text-color);
  855. }
  856.  
  857. .fcxs__updated-message {
  858. margin: 10px 0;
  859. text-align: center;
  860. }
  861.  
  862. .fcxs__updated-message p {
  863. font-size: 14px;
  864. color: var(--error);
  865. }
  866.  
  867. .fcxs__updated-message button {
  868. margin: 14px auto 0;
  869. }
  870.  
  871. /* --- Gallery --- */
  872. .fct-gallery-open,
  873. body.fct-gallery-open,
  874. body.fct-gallery-open #mainPanel {
  875. overflow: hidden!important;
  876. position: fixed!important; //fuck you, stop scolling cunt!
  877. }
  878.  
  879. body.fct-gallery-open fullchan-x {
  880. display: none;
  881. }
  882.  
  883. fullchan-x-gallery {
  884. position: fixed;
  885. top: 0;
  886. left: 0;
  887. width: 100%;
  888. background: rgba(0,0,0,0.9);
  889. display: none;
  890. height: 100%;
  891. overflow: auto;
  892. }
  893.  
  894. fullchan-x-gallery.open {
  895. display: block;
  896. }
  897.  
  898. fullchan-x-gallery .gallery {
  899. padding: 50px 10px 0
  900. }
  901.  
  902. fullchan-x-gallery .gallery__images {
  903. display: flex;
  904. width: 100%;
  905. height: 100%;
  906. justify-content: center;
  907. align-content: flex-start;
  908. gap: 4px 8px;
  909. flex-wrap: wrap;
  910. }
  911.  
  912. fullchan-x-gallery .imgLink img {
  913. border: solid white 1px;
  914. }
  915.  
  916. fullchan-x-gallery .imgLink[data-filemime="video/webm"] img {
  917. border: solid #68b723 4px;
  918. }
  919.  
  920. fullchan-x-gallery .gallery__close {
  921. position: fixed;
  922. top: 60px;
  923. right: 35px;
  924. padding: 6px 14px;
  925. min-height: 30px;
  926. z-index: 10;
  927. }
  928.  
  929. .gallery__main-image {
  930. display: none;
  931. position: fixed;
  932. top: 0;
  933. left: 0;
  934. width: 100%;
  935. height: 100%;
  936. justify-content: center;
  937. align-content: center;
  938. background: rgba(0,0,0,0.5);
  939. }
  940.  
  941. .gallery__main-image img {
  942. padding: 40px 10px 15px;
  943. height: auto;
  944. max-width: calc(100% - 20px);
  945. object-fit: contain;
  946. }
  947.  
  948. .gallery__main-image.active {
  949. display: flex;
  950. }
  951.  
  952. /*-- Truncated file extentions --*/
  953. .originalNameLink[data-file-ext] {
  954. max-width: 65px;
  955. }
  956.  
  957. a[data-file-ext]:hover:after {
  958. content: attr(data-file-ext);
  959. }
  960.  
  961. a[data-file-ext] + .file-ext {
  962. pointer-events: none;
  963. }
  964.  
  965. a[data-file-ext]:hover + .file-ext {
  966. display: none;
  967. }
  968.  
  969. /*-- Nav Board Links --*/
  970. .nav-boards--custom {
  971. display: flex;
  972. gap: 3px;
  973. }
  974.  
  975. #navTopBoardsSpan.hidden ~ #navBoardsSpan,
  976. #navTopBoardsSpan.hidden ~ .nav-fade,
  977. #navTopBoardsSpan a.hidden + span {
  978. display: none;
  979. }
  980. `;
  981.  
  982. document.head.appendChild(style);
  983.  
  984.  
  985. // Asuka and Eris (fantasy Asuka) are best girls