Fullchan X

16/04/2025, 18:06:52

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

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