By V.Boss

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();