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