By V.Boss

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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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