By V.Boss

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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);
})();