Fullchan X

16/04/2025, 18:06:52

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

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