Greasy Fork is available in English.

By V.Boss

Автоматическая проверка объёма, шрифта, размера и наличия фото в темах с точным подсчётом слов. Создано V.Boss. ✨

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         By V.Boss
// @namespace    http://tampermonkey.net/
// @version      44
// @description  Автоматическая проверка объёма, шрифта, размера и наличия фото в темах с точным подсчётом слов. Создано V.Boss. ✨
// @author       V.Boss
// @match        https://forum.blackrussia.online/*
// @grant        none
// ==/UserScript==

(function () {
'use strict';
const ALLOWED_FONTS = ['verdana', 'times new roman'], LS_KEY = 'br_check_v30_clean', LS_HIDE_KEY = 'br_indicator_collapsed';

const settings = Object.assign({checkFont:true, checkSize:true, checkVolume:true, checkPhoto:true, showNotif:true, explosion:true, deviceMode:'pc'}, JSON.parse(localStorage.getItem(LS_KEY) || '{}'));
let lastTopicId = null, notifTimeout = null, notifEl = null, obsTimer = null;
const saveSettings = () => localStorage.setItem(LS_KEY, JSON.stringify(settings));

const style = document.createElement('style');
style.textContent = `
.br-ui-element { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; box-sizing: border-box; }
@keyframes br-fade-up { from { opacity: 0; transform: translate(-50%, 20px); } to { opacity: 1; transform: translate(-50%, 0); } }
@keyframes br-scale-in { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.br-bad-size-text { text-decoration: underline wavy #ff453a 2px !important; background-color: rgba(255, 69, 58, 0.08) !important; position: relative; }
.br-good-size-text { text-decoration: underline wavy #30d158 1.5px !important; background-color: rgba(48, 209, 88, 0.05) !important; }
.br-font-label { display: inline-block; background: #ff453a !important; color: #fff !important; font-size: 10px !important; font-weight: bold !important; padding: 2px 6px !important; border-radius: 4px !important; margin-left: 6px !important; vertical-align: middle; white-space: nowrap; }
.br-gear { position: fixed; top: 20px; right: 20px; z-index: 99999; width: 44px; height: 44px; background: rgba(28, 28, 30, 0.6); backdrop-filter: blur(20px); color: #fff; font-size: 22px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 4px 12px rgba(0,0,0,0.2); transition: all 0.3s; }
.br-gear:hover { transform: rotate(90deg); background: rgba(50, 50, 50, 0.8); }
.br-settings { position: fixed; z-index: 99998; display: none; background: rgba(30, 30, 30, 0.85); backdrop-filter: blur(35px); border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 20px 50px rgba(0,0,0,0.6); padding: 20px; color: #fff; animation: br-scale-in 0.3s; }
.br-settings.show { display: block; }
.br-mode-pc .br-settings { top: 80px; right: 20px; width: 280px; border-radius: 18px; }
.br-mode-mobile .br-settings { bottom: 20px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 400px; border-radius: 24px; animation: br-fade-up 0.4s; }
.br-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
.br-title { font-weight: 600; font-size: 17px; }
.br-close { color: #8e8e93; cursor: pointer; font-size: 24px; padding: 5px; }
.br-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.br-switch { position: relative; display: inline-block; width: 42px; height: 24px; }
.br-switch input { opacity: 0; width: 0; height: 0; }
.br-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #3a3a3c; transition: .4s; border-radius: 24px; }
.br-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .br-slider { background-color: #30d158; }
input:checked + .br-slider:before { transform: translateX(18px); }
.br-notif { position: fixed; left: 50%; bottom: 40px; transform: translate(-50%, 20px); opacity: 0; transition: all 0.5s; background: rgba(28, 28, 30, 0.9); backdrop-filter: blur(20px); color: #fff; padding: 12px 24px; border-radius: 50px; border: 1px solid rgba(255,255,255,0.1); font-weight: 600; z-index: 100000; pointer-events: none; text-align: center; }
.br-notif.show { opacity: 1; transform: translate(-50%, 0); }
.br-notif.good { border-color: #30d158; color: #30d158; }
.br-notif.bad { border-color: #ff453a; color: #ff453a; }
.br-indicator { position: absolute; top: 10px; right: 10px; padding: 8px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; color: #fff; background: rgba(0,0,0,0.75); backdrop-filter: blur(12px); display: flex; flex-direction: column; align-items: flex-end; gap: 4px; z-index: 50; border: 1px solid rgba(255,255,255,0.1); min-width: 140px; }
.br-indicator .ok { color: #30d158; }
.br-indicator .err { color: #ff453a; }
.br-indicator-content { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; width: 100%; }
.br-toggle-panel-btn { align-self: flex-start; background: rgba(255, 255, 255, 0.15) !important; color: #fff !important; border: none !important; border-radius: 4px !important; font-size: 9px !important; text-transform: uppercase !important; padding: 2px 6px !important; cursor: pointer !important; margin-bottom: 4px !important; font-weight: bold !important; }
.br-indicator.collapsed { padding: 4px 8px !important; min-width: auto !important; background: rgba(0, 0, 0, 0.9) !important; }
.br-indicator.collapsed .br-indicator-content { display: none !important; }
.br-indicator.collapsed .br-toggle-panel-btn { margin-bottom: 0 !important; background: transparent !important; font-size: 11px !important; padding: 2px 4px !important; }
.br-lines-sizes { font-size: 11px; opacity: 0.9; margin-top: 5px; border-top: 1px solid rgba(255,255,255,0.15); padding-top: 4px; width: 100%; text-align: right; }
.br-alert-text { color: #ff453a; font-size: 11px; margin-top: 2px; font-weight: bold; width: 100%; text-align: right; }
`;
document.head.appendChild(style);

const gear = document.createElement('div'); gear.className='br-gear br-ui-element'; gear.innerHTML='⚙️';
document.body.appendChild(gear);

const settingsPanel = document.createElement('div'); settingsPanel.className='br-settings br-ui-element';
const createToggle = (id, label, checked) => `<div class="br-row"><span class="br-label">${label}</span><label class="br-switch"><input type="checkbox" id="${id}" ${checked?'checked':''}><span class="br-slider"></span></label></div>`;

settingsPanel.innerHTML = `
    <div class="br-header"><span class="br-title">Настройки</span><span class="br-close">✕</span></div>
    <div class="br-row"><select id="deviceSelector" style="width:100%; padding:8px; border-radius:8px; background:#1c1c1e; color:white; border:none;"><option value="pc">🖥️ PC</option><option value="mobile">📱 Mobile</option></select></div>
    ${createToggle('fontCheck', 'Шрифт', settings.checkFont)}${createToggle('sizeCheck', 'Размер', settings.checkSize)}${createToggle('volCheck', 'Объём (200-600)', settings.checkVolume)}${createToggle('photoCheck', 'Фото / Ссылки', settings.checkPhoto)}
    <div style="border-top:1px solid rgba(255,255,255,0.1); margin:15px 0;"></div>
    ${createToggle('notifCheck', 'Уведомления', settings.showNotif)}${createToggle('explosionCheck', 'Салют', settings.explosion)}
`;
document.body.appendChild(settingsPanel);

const applyMode = () => { document.body.classList.remove('br-mode-pc', 'br-mode-mobile'); document.body.classList.add('br-mode-' + settings.deviceMode); };
gear.onclick = (e) => { e.stopPropagation(); settingsPanel.classList.toggle('show'); };
settingsPanel.querySelector('.br-close').onclick = () => settingsPanel.classList.remove('show');
document.addEventListener('click', (e) => { if (!settingsPanel.contains(e.target) && e.target !== gear) settingsPanel.classList.remove('show'); });

const devSel = document.getElementById('deviceSelector'); devSel.value = settings.deviceMode;
devSel.onchange = () => { settings.deviceMode = devSel.value; saveSettings(); applyMode(); };
settingsPanel.onchange = (e) => {
    if(e.target.tagName === 'SELECT') return;
    settings.checkFont = document.getElementById('fontCheck').checked; settings.checkSize = document.getElementById('sizeCheck').checked;
    settings.checkVolume = document.getElementById('volCheck').checked; settings.checkPhoto = document.getElementById('photoCheck').checked;
    settings.showNotif = document.getElementById('notifCheck').checked; settings.explosion = document.getElementById('explosionCheck').checked;
    saveSettings(); runCheck();
};
applyMode();

function notify(htmlMsg, type) {
    if(!settings.showNotif) return;
    if(!notifEl){ notifEl=document.createElement('div'); notifEl.className='br-notif br-ui-element'; document.body.appendChild(notifEl); }
    if (notifEl.innerHTML === htmlMsg && notifEl.classList.contains('show')) return;
    notifEl.innerHTML = htmlMsg; notifEl.className=`br-notif br-ui-element ${type} show`;
    clearTimeout(notifTimeout); notifTimeout = setTimeout(()=> { notifEl.classList.remove('show'); }, 5000);
}

function getTextNodes(node) {
    let textNodes = [];
    if (node.nodeType === Node.TEXT_NODE) { if (node.nodeValue.trim().length > 0) textNodes.push(node); }
    else if (node.nodeType === Node.ELEMENT_NODE && !node.classList.contains('br-ui-element') && !node.classList.contains('br-indicator') && !node.classList.contains('br-font-label')) {
        for (let child of node.childNodes) textNodes.push(...getTextNodes(child));
    }
    return textNodes;
}

function analyzeTopPost() {
    const wrapper = document.querySelector('.bbWrapper'); if (!wrapper) return;
    let resultHTML = "", allPassed = true, problems = [], sizeDetailsHTML = "";
    wrapper.querySelectorAll('.br-bad-size-text, .br-good-size-text').forEach(el => el.replaceWith(...el.childNodes));
    wrapper.querySelectorAll('.br-font-label').forEach(el => el.remove()); wrapper.normalize();

    // Новая строгая очистка текста перед подсчётом
    let textClean = wrapper.textContent
        .replace(/https?:\/\/\S+/gi, '') // Полностью вырезаем любые интернет-ссылки
        .replace(/[\u00A0\t\n\r]/g, ' ') // Убираем жесткие переносы строк и табы
        .replace(/[^а-яА-ЯёЁa-zA-Z\s-]/g, ''); // Удаляем цифры, знаки препинания и спецсимволы

    const words = textClean.match(/[а-яА-ЯёЁa-zA-Z-]{2,}/g); // Считаем слова только от 2-х букв (игнорируем обрывки букв)
    const wordCount = words ? words.length : 0;

    if (settings.checkFont) {
        const comp = getComputedStyle(wrapper), fonts = (comp.fontFamily || '').toLowerCase();
        let fontOK = ALLOWED_FONTS.some(f => fonts.includes(f));
        if (!fontOK) {
             const innerFontEl = wrapper.querySelector('[style*="font-family"]');
             if(innerFontEl && ALLOWED_FONTS.some(f => innerFontEl.style.fontFamily.toLowerCase().includes(f))) fontOK = true;
        }
        resultHTML += `<span class="${fontOK?'ok':'err'}">${fontOK?'✅':'❌'} Шрифт</span>`; if (!fontOK) { allPassed = false; problems.push("Шрифт"); }
    }

    if (settings.checkSize || settings.checkFont) {
        let globalSizeOK = true, uniqueSizes = new Set(), badSizesFound = new Set();
        getTextNodes(wrapper).forEach(node => {
            const parent = node.parentElement;
            if (parent) {
                const comp = getComputedStyle(parent), size = Math.round(parseFloat(comp.fontSize)), fontRaw = comp.fontFamily || '';
                if (size > 0) {
                    uniqueSizes.add(size);
                    const span = document.createElement('span'), hasSizeErr = size < 15, hasFontErr = !ALLOWED_FONTS.some(f => fontRaw.toLowerCase().includes(f));
                    let isBad = false;
                    
                    if ((settings.checkSize && hasSizeErr) || (settings.checkFont && hasFontErr)) {
                        if (hasSizeErr) { globalSizeOK = false; badSizesFound.add(size); }
                        span.className = 'br-bad-size-text'; isBad = true;
                    } else { span.className = 'br-good-size-text'; }
                    
                    node.before(span); span.appendChild(node);
                    if (isBad) {
                        const label = document.createElement('span'); label.className = 'br-font-label br-ui-element';
                        label.textContent = `[${fontRaw.replace(/['"]/g, '').split(',')[0].trim()} | ${size}px]`; span.after(label);
                    }
                }
            }
        });
        if (uniqueSizes.size === 0) {
            const base = Math.round(parseFloat(getComputedStyle(wrapper).fontSize)); uniqueSizes.add(base);
            globalSizeOK = base >= 15; if(!globalSizeOK) { badSizesFound.add(base); allPassed = false; problems.push("Размер < 15px"); }
        } else if (!globalSizeOK) { allPassed = false; problems.push("Размер < 15px"); }

        if (settings.checkSize) {
            resultHTML += `<span class="${globalSizeOK?'ok':'err'}">${globalSizeOK?'✅':'❌'} Размер</span>`;
            sizeDetailsHTML = `<div class="br-lines-sizes">В тексте: ` + Array.from(uniqueSizes).sort((a,b)=>a-b).map(s => `<b class="${s>=15?'ok':'err'}">${s}px</b>`).join(', ') + `</div>`;
            if (badSizesFound.size > 0) sizeDetailsHTML += `<div class="br-alert-text">⚠️ Мелкий текст: ${Array.from(badSizesFound).sort((a,b)=>a-b).map(s=>s+'px').join(', ')}!</div>`;
        }
    }

    if (settings.checkVolume) {
        const volOK = (wordCount >= 200 && wordCount <= 600);
        resultHTML += `<span class="${volOK?'ok':'err'}">${volOK?'✅':'❌'} Объём: ${wordCount}</span>`;
        if (!volOK) { allPassed = false; problems.push("Объём"); }
    }
    if (settings.checkPhoto) {
        const photoOK = !!(wrapper.querySelector('img[src*="http"]') || Array.from(wrapper.querySelectorAll('a')).some(a => (a.href && a.href.includes('http'))));
        resultHTML += `<span class="${photoOK?'ok':'err'}">${photoOK?'✅':'❌'} Фото/Ссылки</span>`;
        if (!photoOK) { allPassed = false; problems.push("Нет фото"); }
    }

    let ind = wrapper.querySelector('.br-indicator');
    if (!ind) {
        ind = document.createElement('div'); ind.className = 'br-indicator br-ui-element';
        if(getComputedStyle(wrapper).position === 'static') wrapper.style.position = 'relative';
        const btn = document.createElement('button'); btn.className = 'br-toggle-panel-btn br-ui-element';
        if (localStorage.getItem(LS_HIDE_KEY) === 'true') { ind.classList.add('collapsed'); btn.textContent = '👁️ Показать'; } else { btn.textContent = 'Скрыть'; }
        btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const c = ind.classList.toggle('collapsed'); localStorage.setItem(LS_HIDE_KEY, c); btn.textContent = c ? '👁️ Показать' : 'Скрыть'; };
        const cont = document.createElement('div'); cont.className = 'br-indicator-content br-ui-element';
        ind.appendChild(btn); ind.appendChild(cont); wrapper.appendChild(ind);
    }
    const cc = ind.querySelector('.br-indicator-content'); if (cc) cc.innerHTML = resultHTML + sizeDetailsHTML;

    if (allPassed) {
        notify("✅ Биография одобрена.", "good");
        if (settings.explosion && lastTopicId !== location.pathname) { lastTopicId = location.pathname; createFireworks(); }
    } else { notify("❌ Исправьте: " + problems.join(", "), "bad"); }
}

function createFireworks() {
    const colors = ['#30d158', '#ffd60a', '#ff453a', '#0a84ff', '#fff'], container = document.createElement('div');
    container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:100002;'; document.body.appendChild(container);
    for (let i=0; i<40; i++) {
        const p = document.createElement('div'), angle = Math.random() * Math.PI * 2, dist = 100 + Math.random() * 300;
        p.style.cssText = `position:absolute;top:50%;left:50%;width:6px;height:6px;border-radius:50%;background:${colors[Math.floor(Math.random()*colors.length)]};transition:1s ease-out;`;
        container.appendChild(p);
        requestAnimationFrame(()=>{ p.style.transform = `translate(${Math.cos(angle)*dist}px, ${Math.sin(angle)*dist}px) scale(0)`; p.style.opacity = 0; });
    }
    setTimeout(()=>container.remove(), 1200);
}

const runCheck = () => analyzeTopPost();
const observer = new MutationObserver((mutations)=>{
    let run = false;
    for(let m of mutations) {
        if((m.target.className && typeof m.target.className === 'string' && m.target.className.includes('br-')) || (m.target.closest && m.target.closest('.br-indicator'))) continue;
        run = true;
    }
    if(run) { clearTimeout(obsTimer); obsTimer=setTimeout(runCheck, 600); } // Ускорили задержку для мгновенной фиксации изменений
});
setTimeout(() => { observer.observe(document.body, {childList:true, subtree:true}); runCheck(); }, 2000);
})();