Guest User

Fullchan X /alter/

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