ifgodisn3

neochan userscript

Aug 15th, 2026 (edited)
97
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         Neochan extension
  3. // @namespace    http://tampermonkey.net/
  4. // @version      1.5.2
  5. // @description  Расширение неочана
  6. // @match        https://neochan.org/*/*
  7. // @match        https://neochan.net/*/*
  8. // @match        https://neochan.cc/*/*
  9. // @grant        none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13.     'use strict';
  14.  
  15.     function resetPreviewFileBlock(fileBlock) {
  16.         fileBlock.querySelectorAll('.post-file-name-visible').forEach(el => el.remove());
  17.         fileBlock.querySelectorAll('.tiktok-icon').forEach(el => el.remove());
  18.         fileBlock.classList.remove('fnAdded');
  19.     }
  20.  
  21.     function decodeHtml(str) {
  22.         const parser = new DOMParser();
  23.         const doc = parser.parseFromString(str, "text/html");
  24.         return doc.documentElement.textContent;
  25.     }
  26.     function processFiles() {
  27.         document.querySelectorAll('.post-file').forEach(fileBlock => {
  28.             const article = fileBlock.closest('article');
  29.             const isPreview = article && article.classList.contains('hover');
  30.  
  31.             if (isPreview) {
  32.                  if (fileBlock.dataset.fnPreviewAdded) return;
  33.                 fileBlock.dataset.fnPreviewAdded = "1";
  34.                 resetPreviewFileBlock(fileBlock);
  35.             } else {
  36.                 if (fileBlock.dataset.fnAdded) return;
  37.                 fileBlock.dataset.fnAdded = "1";
  38.             }
  39.  
  40.  
  41.             const link = fileBlock.querySelector('.post-file-link');
  42.             if (!link) return;
  43.  
  44.             const originalName = decodeHtml(link.getAttribute('name'));
  45.             if (!originalName) return;
  46.  
  47.             const infoDiv = link.getAttribute('data-func') !== 'PlayFirstEmbed' ? link.parentElement.querySelector('.post-file-info') : null;
  48.             if (!infoDiv) return;
  49.  
  50.             const nameDiv = document.createElement('div');
  51.  
  52.             nameDiv.style.cursor = 'pointer';
  53.             nameDiv.addEventListener('click', () => {
  54.                 const a = document.createElement('a');
  55.                 a.href = link.href;
  56.                 a.download = originalName;
  57.                 document.body.appendChild(a);
  58.                 a.click();
  59.                 a.remove();
  60.             });
  61.  
  62.             nameDiv.addEventListener('contextmenu', (e) => {
  63.                 e.preventDefault();
  64.  
  65.                 navigator.clipboard.writeText(nameDiv.textContent)
  66.                     .then(() => {
  67.                     const toast = document.createElement('div');
  68.                     toast.className = 'copy-toast';
  69.                     toast.textContent = 'Скопировано!';
  70.  
  71.                     const rect = nameDiv.getBoundingClientRect();
  72.                     toast.style.left = rect.left + window.scrollX + 'px';
  73.                     toast.style.top = rect.top + window.scrollY - 30 + 'px';
  74.  
  75.                     document.body.appendChild(toast);
  76.  
  77.                     requestAnimationFrame(() => {
  78.                         toast.classList.add('visible');
  79.                     });
  80.  
  81.                     setTimeout(() => {
  82.                         toast.classList.remove('visible');
  83.                         setTimeout(() => toast.remove(), 200);
  84.                     }, 1000);
  85.                 });
  86.             });
  87.  
  88.  
  89.             nameDiv.className = 'post-file-name-visible';
  90.             nameDiv.textContent = originalName;
  91.             nameDiv.title = originalName;
  92.  
  93.             const computed = getComputedStyle(infoDiv);
  94.             nameDiv.style.color = computed.color;
  95.  
  96.             const preview = fileBlock.querySelector('img.preview');
  97.             if (preview) nameDiv.style.maxWidth = preview.width + 'px';
  98.  
  99.             const showNames = localStorage.getItem('showFilenames') === '1';
  100.             nameDiv.style.display = showNames ? '' : 'none';
  101.  
  102.             infoDiv.insertAdjacentElement('afterend', nameDiv);
  103.  
  104.             const showTikTok = localStorage.getItem('showTikTokIcons') === '1';
  105.             const match = originalName.match(/[67]\d{18}/);
  106.  
  107.             if (match && showTikTok) {
  108.                 const id = match[0];
  109.  
  110.                 const icon = document.createElement('span');
  111.                 icon.className = 'tiktok-icon';
  112.                 icon.title = "Открыть TikTok видео";
  113.                 icon.style.cursor = 'pointer';
  114.                 icon.style.marginLeft = '6px';
  115.                 icon.style.opacity = '0.8';
  116.  
  117.                 icon.innerHTML = `
  118.                     <svg width="16" height="16" viewBox="0 0 48 48">
  119.                         <path fill="currentColor" d="M30 4h6c0 4 3 8 8 8v6c-4 0-8-2-11-5v17a14 14 0 1 1-14-14h2v6h-2a8 8 0 1 0 8 8V4z"/>
  120.                     </svg>
  121.                 `;
  122.  
  123.                 icon.addEventListener('click', () => {
  124.                     window.open(`https://www.tiktok.com/@/video/${id}`, '_blank');
  125.                 });
  126.  
  127.                 infoDiv.appendChild(icon);
  128.             }
  129.  
  130.             const showGIFs = localStorage.getItem('showGIFs') === '1';
  131.             const href = link.getAttribute('href');
  132.  
  133.             if (href?.endsWith('.gif') && showGIFs) {
  134.                 const img = fileBlock.querySelector('img.preview');
  135.                 if (img) {
  136.                     const marker = fileBlock.querySelector('.post-gif-marker');
  137.                     if (marker) marker.remove();
  138.  
  139.                     img.src = img.src
  140.                         .replace('/thumb/', '/src/')
  141.                         .replace(/\.\w+$/, '.gif');
  142.                 }
  143.             }
  144.         });
  145.     }
  146.  
  147.     function toggleFilenames(enabled) {
  148.         document.querySelectorAll('.post-file-name-visible').forEach(el => {
  149.             el.style.display = enabled ? '' : 'none';
  150.         });
  151.     }
  152.  
  153.     function toggleGIFs(enabled) {
  154.         document.querySelectorAll('.post-file').forEach(fileBlock => {
  155.             const link = fileBlock.querySelector('.post-file-link');
  156.             if (!link) return;
  157.  
  158.             const href = link.getAttribute('href');
  159.             if (!href || !href.endsWith('.gif')) return;
  160.  
  161.             const img = fileBlock.querySelector('img.preview');
  162.             const marker = fileBlock.querySelector('.post-gif-marker');
  163.  
  164.             if (enabled) {
  165.                 if (marker) marker.style.display = 'none';
  166.  
  167.                 if (img) {
  168.                     img.src = img.src
  169.                         .replace('/thumb/', '/src/')
  170.                         .replace(/\.\w+$/, '.gif');
  171.                 }
  172.             } else {
  173.                 if (marker) marker.style.display = '';
  174.  
  175.                 if (img) {
  176.                     img.src = img.src
  177.                         .replace('/src/', '/thumb/')
  178.                         .replace(/\.gif$/, '.jpg');
  179.                 }
  180.             }
  181.         });
  182.     }
  183.  
  184.     function toggleTikTokIcons(enabled) {
  185.         document.querySelectorAll('.tiktok-icon').forEach(el => {
  186.             el.style.display = enabled ? '' : 'none';
  187.         });
  188.     }
  189.  
  190.     function fixPlayerDownloadButton() {
  191.         const player = document.querySelector('#player-container');
  192.         if (!player) return;
  193.  
  194.         const video = player.querySelector('video');
  195.         if (!video) return;
  196.  
  197.         const source = video.querySelector('source');
  198.         if (!source) return;
  199.  
  200.         const src = source.getAttribute('src');
  201.         if (!src) return;
  202.  
  203.         const previewLink = document.querySelector(`.post-file-link[href="${src}"]`);
  204.         if (!previewLink) return;
  205.  
  206.         const originalName = previewLink.getAttribute('name');
  207.         if (!originalName) return;
  208.  
  209.         const btn = player.querySelector('#plyr-download');
  210.         if (!btn) return;
  211.  
  212.         const parentButton = btn.closest('button');
  213.         if (!parentButton) return;
  214.  
  215.         parentButton.onclick = (e) => {
  216.             e.stopPropagation();
  217.             e.preventDefault();
  218.  
  219.             const a = document.createElement('a');
  220.             a.href = src;
  221.             a.download = originalName;
  222.             document.body.appendChild(a);
  223.             a.click();
  224.             a.remove();
  225.         };
  226.     }
  227.  
  228.     function filterThreads() {
  229.         const enabled = localStorage.getItem('threadFilterEnabled') === '1';
  230.         const rawFilter = (localStorage.getItem('threadFilterText') || '').trim();
  231.  
  232.         document.querySelectorAll('#index-thread-container .thread').forEach(thread => {
  233.             thread.style.display = '';
  234.             const next = thread.nextElementSibling;
  235.             if (next && next.tagName === 'HR') {
  236.                 next.style.display = '';
  237.             }
  238.  
  239.             if (!enabled || !rawFilter) return;
  240.  
  241.             let text = '';
  242.  
  243.             const subject = thread.querySelector('.post-subject');
  244.             if (subject) text += ' ' + (subject.textContent || '');
  245.  
  246.             const messages = thread.querySelectorAll('.post_op .post-message');
  247.             messages.forEach(msg => {
  248.                 text += ' ' + (msg.textContent || '');
  249.             });
  250.  
  251.             let isMatch = false;
  252.  
  253.             const regexMatch = rawFilter.match(/^\/(.+)\/([gimsuy]*)$/);
  254.  
  255.             if (regexMatch) {
  256.                 try {
  257.                     const regex = new RegExp(regexMatch[1], regexMatch[2]);
  258.                     isMatch = regex.test(text);
  259.                 } catch (e) {
  260.                     isMatch = text.toLowerCase().includes(rawFilter.toLowerCase());
  261.                 }
  262.             } else {
  263.                 isMatch = text.toLowerCase().includes(rawFilter.toLowerCase());
  264.             }
  265.  
  266.             if (isMatch) {
  267.                 thread.style.display = 'none';
  268.  
  269.                 if (next && next.tagName === 'HR') {
  270.                     next.style.display = 'none';
  271.                 }
  272.             }
  273.         });
  274.     }
  275.  
  276.     function addOptionCheckbox() {
  277.         const options = document.querySelector('#options .options-tab');
  278.         if (!options || document.querySelector('#optionShowFilenames')) return;
  279.  
  280.         const header = document.createElement('div');
  281.         header.className = 'options-item';
  282.         header.style.marginTop = '10px';
  283.         header.innerHTML = `<span style="font-weight: bold; opacity: 0.8;">Расширение</span>`;
  284.         options.appendChild(header);
  285.  
  286.         const hr = document.createElement('hr');
  287.         hr.style.margin = '5px 0 10px 0';
  288.         options.appendChild(hr);
  289.  
  290.         const item1 = document.createElement('div');
  291.         item1.className = 'options-item';
  292.         item1.innerHTML = `
  293.             <label class="checktainer_xs">
  294.                 Показывать имена файлов
  295.                 <input type="checkbox" id="optionShowFilenames">
  296.                 <span class="checkmark_xs"></span>
  297.             </label>
  298.         `;
  299.         options.appendChild(item1);
  300.  
  301.         const item2 = document.createElement('div');
  302.         item2.className = 'options-item';
  303.         item2.innerHTML = `
  304.             <label class="checktainer_xs">
  305.                 Показывать соус TikTok-ов
  306.                 <input type="checkbox" id="optionShowTikTokIcons">
  307.                 <span class="checkmark_xs"></span>
  308.             </label>
  309.         `;
  310.         options.appendChild(item2);
  311.  
  312.         const showNames = localStorage.getItem('showFilenames') === '1';
  313.         const showTikTok = localStorage.getItem('showTikTokIcons') === '1';
  314.  
  315.         const cb1 = document.querySelector('#optionShowFilenames');
  316.         const cb2 = document.querySelector('#optionShowTikTokIcons');
  317.  
  318.         cb1.checked = showNames;
  319.         cb2.checked = showTikTok;
  320.  
  321.         cb1.addEventListener('change', e => {
  322.             const state = e.target.checked;
  323.             localStorage.setItem('showFilenames', state ? '1' : '0');
  324.             toggleFilenames(state);
  325.         });
  326.  
  327.         cb2.addEventListener('change', e => {
  328.             const state = e.target.checked;
  329.             localStorage.setItem('showTikTokIcons', state ? '1' : '0');
  330.             toggleTikTokIcons(state);
  331.         });
  332.  
  333.         const item3 = document.createElement('div');
  334.         item3.className = 'options-item';
  335.         item3.innerHTML = `
  336.     <label class="checktainer_xs">
  337.         Показывать GIF
  338.         <input type="checkbox" id="optionShowGIFs">
  339.         <span class="checkmark_xs"></span>
  340.     </label>
  341. `;
  342.         options.appendChild(item3);
  343.  
  344.         const showGIFs = localStorage.getItem('showGIFs') === '1';
  345.         const cb3 = document.querySelector('#optionShowGIFs');
  346.         cb3.checked = showGIFs;
  347.  
  348.         cb3.addEventListener('change', e => {
  349.             const state = e.target.checked;
  350.             localStorage.setItem('showGIFs', state ? '1' : '0');
  351.             toggleGIFs(state);
  352.         });
  353.  
  354.         const item4 = document.createElement('div');
  355.         item4.className = 'options-item';
  356.         item4.innerHTML = `
  357.     <label class="checktainer_xs">
  358.         Удалять имена загружаемых файлов
  359.         <input type="checkbox" id="optionRandomizeFilenames">
  360.         <span class="checkmark_xs"></span>
  361.     </label>
  362. `;
  363.         options.appendChild(item4);
  364.  
  365.         const randomEnabled = localStorage.getItem('randomizeFilenames') === '1';
  366.         const cb4 = document.querySelector('#optionRandomizeFilenames');
  367.         cb4.checked = randomEnabled;
  368.  
  369.         cb4.addEventListener('change', e => {
  370.             const state = e.target.checked;
  371.             localStorage.setItem('randomizeFilenames', state ? '1' : '0');
  372.         });
  373.  
  374.         const item5 = document.createElement('div');
  375.         item5.className = 'options-item';
  376.         item5.style.display = 'flex';
  377.         item5.style.alignItems = 'center';
  378.         item5.style.gap = '8px';
  379.         item5.style.flexWrap = 'wrap';
  380.  
  381.         item5.innerHTML = `
  382.             <label class="checktainer_xs" style="margin-right: 4px;">
  383.                 Фильтр тредов
  384.                 <input type="checkbox" id="optionThreadFilter">
  385.                 <span class="checkmark_xs"></span>
  386.             </label>
  387.             <input type="text" id="optionThreadFilterText" placeholder="Текст для скрытия..."
  388.                    style="flex: 1; min-width: 120px; padding: 3px 6px; font-size: 12px; border-radius: 3px; border: 1px solid #666; background: #222; color: #eee;">
  389.         `;
  390.         options.appendChild(item5);
  391.  
  392.         const filterEnabled = localStorage.getItem('threadFilterEnabled') === '1';
  393.         const filterText = localStorage.getItem('threadFilterText') || '';
  394.  
  395.         const cb5 = document.querySelector('#optionThreadFilter');
  396.         const input5 = document.querySelector('#optionThreadFilterText');
  397.  
  398.         cb5.checked = filterEnabled;
  399.         input5.value = filterText;
  400.  
  401.         cb5.addEventListener('change', e => {
  402.             const state = e.target.checked;
  403.             localStorage.setItem('threadFilterEnabled', state ? '1' : '0');
  404.             filterThreads();
  405.         });
  406.  
  407.         input5.addEventListener('input', e => {
  408.             localStorage.setItem('threadFilterText', e.target.value);
  409.             filterThreads();
  410.         });
  411.  
  412.         filterThreads();
  413.     }
  414.  
  415.     const style = document.createElement('style');
  416.     style.textContent = `
  417.         .post-file-name-visible {
  418.             white-space: nowrap;
  419.             overflow: hidden;
  420.             text-overflow: ellipsis;
  421.             font-size: smaller;
  422.             font-style: italic;
  423.             padding-right: 5px;
  424.             transition: filter 0.15s ease-in-out;
  425.             cursor: pointer;
  426.         }
  427.         .post-file-name-visible:hover {
  428.             transform: scale(1.05);
  429.         }
  430.         .tiktok-icon svg {
  431.             vertical-align: middle;
  432.         }
  433.         .copy-toast {
  434.     position: absolute;
  435.     padding: 6px 12px;
  436.     background: rgba(0, 0, 0, 0.75);
  437.     color: #fff;
  438.     border-radius: 6px;
  439.     font-size: 13px;
  440.     pointer-events: none;
  441.     opacity: 0;
  442.     transition: opacity 0.2s ease;
  443.     z-index: 9999;
  444. }
  445.  
  446. .copy-toast.visible {
  447.     opacity: 1;
  448. }
  449.     `;
  450.     document.head.appendChild(style);
  451.  
  452.     const styleFix = document.createElement('style');
  453.     styleFix.textContent = `
  454.     .plyr__poster {
  455.         pointer-events: none !important;
  456.     }
  457. `;
  458.     document.head.appendChild(styleFix);
  459.  
  460.     processFiles();
  461.     toggleFilenames(localStorage.getItem('showFilenames') === '1');
  462.     toggleTikTokIcons(localStorage.getItem('showTikTokIcons') === '1');
  463.     toggleGIFs(localStorage.getItem('showGIFs') === '1');
  464.     addOptionCheckbox();
  465.     filterThreads();
  466.  
  467.     const observer = new MutationObserver(() => {
  468.         processFiles();
  469.         addOptionCheckbox();
  470.         fixPlayerDownloadButton();
  471.         filterThreads();
  472.     });
  473.     observer.observe(document.body, { childList: true, subtree: true });
  474. })();
  475.  
  476.  
  477. (function interceptXHR() {
  478.     const origSend = XMLHttpRequest.prototype.send;
  479.  
  480.     XMLHttpRequest.prototype.send = function(body) {
  481.         try {
  482.             const enabled = localStorage.getItem('randomizeFilenames') === '1';
  483.  
  484.             if (body instanceof FormData) {
  485.                 const newForm = new FormData();
  486.  
  487.                 for (const [key, value] of body.entries()) {
  488.                     if (value instanceof File) {
  489.  
  490.                         let ext = value.name.match(/\.[a-zA-Z0-9]+$/)?.[0] || '';
  491.  
  492.                         if (ext.toLowerCase() === '.mkv') {
  493.                             ext = '.mp4';
  494.                         }
  495.  
  496.                         let newName;
  497.  
  498.                         if (enabled) {
  499.                             newName = Date.now() - Math.round(Math.random() * 15768e7) + ext;
  500.                         } else {
  501.                             newName = value.name.replace(/\.[a-zA-Z0-9]+$/, ext);
  502.                         }
  503.  
  504.                         const newFile = new File([value], newName, { type: value.type });
  505.  
  506.                         newForm.append(key, newFile);
  507.                     } else {
  508.                         newForm.append(key, value);
  509.                     }
  510.                 }
  511.  
  512.                 return origSend.call(this, newForm);
  513.             }
  514.         } catch (e) {
  515.             console.error("Filename randomizer error:", e);
  516.         }
  517.  
  518.         return origSend.call(this, body);
  519.     };
  520. })();
Advertisement
Add Comment
Please, Sign In to add comment