BetterMangaBuff

Синхронный ZIP-движок. Умные закладки (точный фикс нумерации страниц). Скачивание прямо из списка глав (исправлено для дробных глав). Смайлы в чате (работают и при ответах). Фикс шапки. Сетка для закладок.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         BetterMangaBuff
// @namespace    https://mangabaf-dl
// @version      4.2
// @description  Синхронный ZIP-движок. Умные закладки (точный фикс нумерации страниц). Скачивание прямо из списка глав (исправлено для дробных глав). Смайлы в чате (работают и при ответах). Фикс шапки. Сетка для закладок.
// @author       countlynxz
// @match        *://mangabuff.ru/*
// @match        *://*.mangabuff.ru/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      *
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const isChapterPage = () => location.pathname.includes('/manga/') && /\/\d+/.test(location.pathname);

    // =========================================================================
    // 1. СТИЛИ ИНТЕРФЕЙСА
    // =========================================================================
    GM_addStyle(`
        body.mb-reader-mode header.reader__header,
        body.mb-reader-mode .reader__header {
            background: transparent !important; background-color: transparent !important;
            backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
            border-bottom: none !important; box-shadow: none !important;
            display: flex !important; align-items: center !important; justify-content: space-between !important;
        }
        body.mb-reader-mode .reader__header > div {
            background: rgba(20, 20, 20, 0.75) !important;
            backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;
            border: 1px solid rgba(255, 255, 255, 0.05) !important; border-radius: 12px !important;
            padding: 6px 12px !important; transition: all 0.2s ease !important;
        }
        body.mb-reader-mode .reader__header [class*="dropdown-menu"],
        body.mb-reader-mode .reader__header .dropdown-menu {
            background: #141414 !important; background-color: #141414 !important;
            border: 1px solid #262626 !important; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6) !important;
            border-radius: 8px !important; padding: 8px 0 !important;
        }
        body.mb-reader-mode .reader__header [class*="dropdown-menu"] a,
        body.mb-reader-mode .reader__header .dropdown-menu a {
            color: #b3b3b3 !important; padding: 10px 16px !important; display: flex !important;
            align-items: center !important; gap: 10px !important; transition: background 0.15s ease, color 0.15s ease !important;
        }
        body.mb-reader-mode .reader__header [class*="dropdown-menu"] a:hover {
            background: #222 !important; color: #fff !important;
        }

        /* Чат — Фикс фиолетовой рамки везде, включая ответы */
        input[placeholder*="сообщение"], textarea[placeholder*="сообщение"],
        input[placeholder*="твет"], textarea[placeholder*="твет"],
        div:has(> input[placeholder*="сообщение"]), div:has(> textarea[placeholder*="сообщение"]),
        div:has(> input[placeholder*="твет"]), div:has(> textarea[placeholder*="твет"]) {
            border: none !important; outline: none !important; box-shadow: none !important;
        }
        .custom-emoji-btn { background: none; border: none; font-size: 20px; cursor: pointer; padding: 0 8px; transition: transform 0.1s; user-select: none; }
        .custom-emoji-btn:hover { transform: scale(1.15); }
        .custom-emoji-picker { position: absolute; bottom: 50px; left: 10px; background: #1e1e1e;
            border: 1px solid #333; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); padding: 10px; display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px;
            z-index: 99999; max-height: 200px; overflow-y: auto; width: 220px; }
        .custom-emoji-item { font-size: 20px; cursor: pointer; text-align: center; padding: 4px; border-radius: 4px; transition: background 0.1s; user-select: none; }
        .custom-emoji-item:hover { background: #333; }

        /* Кнопки скачивания */
        #mb-download-btn { position: fixed; bottom: 20px; right: 20px; z-index: 99998; background: #ff5c5c; color: #fff; border: none; padding: 12px 20px; border-radius: 8px; font-weight: bold;
            cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.2); transition: all 0.2s ease; }
        #mb-download-btn:hover { background: #e04e4e; transform: scale(1.03); }
        #mb-download-btn.loading { background: #666; cursor: wait; }

        .mb-list-dl-btn { background: rgba(255, 92, 92, 0.1); color: #ff5c5c;
            border: 1px solid rgba(255, 92, 92, 0.3); border-radius: 6px; padding: 4px 10px; cursor: pointer; font-size: 14px; margin-left: 15px;
            transition: all 0.2s ease; z-index: 10; display: inline-flex; align-items: center; justify-content: center; font-weight: bold; }
        .mb-list-dl-btn:hover { background: rgba(255, 92, 92, 0.8); color: #fff; transform: translateY(-1px); }
        .mb-list-dl-btn.loading { background: #666; color: #fff; border-color: #666; cursor: wait; transform: none; }

        /* Умные закладки */
        #mb-bookmark-toast { position: fixed; top: 80px; left: 50%; transform: translateX(-50%); z-index: 99999; background: rgba(20, 20, 20, 0.95); backdrop-filter: blur(8px); border: 1px solid #333;
            color: #fff; padding: 10px 20px; border-radius: 30px; font-size: 14px; box-shadow: 0 10px 25px rgba(0,0,0,0.6); display: flex; align-items: center; gap: 15px;
            animation: mbSlideDown 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
        #mb-bookmark-toast span.mb-bm-link { color: #5eff5e; font-weight: bold; cursor: pointer; text-decoration: underline; transition: color 0.2s; }
        #mb-bookmark-toast span.mb-bm-link:hover { color: #8cff8c; }
        #mb-bookmark-toast span.mb-bm-close { cursor: pointer; color: #888; font-size: 16px; margin-left: 5px; }
        #mb-bookmark-toast span.mb-bm-close:hover { color: #fff; }
        @keyframes mbSlideDown { from { top: -50px; opacity: 0; } to { top: 80px; opacity: 1; } }

        /* Уведомления */
        .mb-toast { position: fixed; bottom: 80px; right: 20px; z-index: 99999; background: #222; color: #fff; padding: 10px 15px; border-radius: 6px; font-size: 14px;
            box-shadow: 0 4px 10px rgba(0,0,0,0.3); animation: mbFadeIn 0.3s ease; }
        @keyframes mbFadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
        .mbd-ok { color: #5eff5e; font-weight: bold; }
        .mbd-err { color: #ff5e5e; font-weight: bold; }

        /* Сетка закладок */
        .bookmark__right { 
            display: grid !important;
            grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)) !important;
            gap: 20px !important;
            align-items: start !important;
        }
        .bookmark__right > .search,
        .bookmark__right > .dropdown,
        .bookmark__right > .bookmark-folder-controls {
            grid-column: 1 / -1 !important;
        }
        .bookmark__manga { 
            display: flex !important;
            flex-direction: column !important;
            background: #141414 !important;
            border-radius: 12px !important;
            overflow: visible !important;
            padding: 0 !important;
            border: 1px solid rgba(255, 255, 255, 0.05) !important;
            transition: transform 0.2s ease, box-shadow 0.2s ease !important;
            position: relative !important;
        }
        .bookmark__manga:hover {
            transform: translateY(-4px) !important;
            box-shadow: 0 10px 20px rgba(0,0,0,0.4) !important;
        }
        .bookmark__manga-container {
            width: 100% !important;
            height: 320px !important;
            min-width: unset !important;
            margin: 0 !important;
            position: relative !important;
            flex-shrink: 0 !important;
            border-radius: 12px 12px 0 0 !important;
            overflow: hidden !important;
        }
        .bookmark__manga-image {
            width: 100% !important;
            height: 100% !important;
            display: block !important;
            background-size: cover !important;
            background-position: center !important;
            border-radius: 0 !important;
        }
        .bookmark__manga-container .custom-checkbox {
            position: absolute !important;
            top: 8px !important;
            left: 8px !important;
            z-index: 10 !important;
            background: rgba(0,0,0,0.6) !important;
            padding: 2px !important;
            border-radius: 6px !important;
        }
        .bookmark__manga-info {
            display: flex !important;
            flex-direction: column !important;
            padding: 10px 12px !important;
            gap: 4px !important;
            background: #141414 !important;
            position: relative !important;
            z-index: 2 !important;
            flex-shrink: 0 !important;
            border-radius: 0 0 12px 12px !important;
            height: 105px !important;
            overflow: hidden !important;
            box-sizing: border-box !important;
        }
        .bookmark__manga-info > *:first-child {
            font-size: 14px !important;
            line-height: 1.3 !important;
            margin: 0 !important;
            height: calc(14px * 1.3 * 2) !important;
            display: -webkit-box !important;
            -webkit-line-clamp: 2 !important;
            -webkit-box-orient: vertical !important;
            overflow: hidden !important;
            flex-shrink: 0 !important;
        }
        .bookmark__manga-info > *:nth-child(2) { font-size: 13px !important; margin: 0 !important; flex-shrink: 0 !important; }
        .bookmark__manga-info > *:nth-child(3) { font-size: 13px !important; margin: 0 !important; flex-shrink: 0 !important; }
        .bookmark__manga-info > *:nth-child(n+4) { display: none !important; }
        .bookmark__manga-right { display: none !important; }
        .bookmark__manga-controls { display: none !important; }
        .bookmark__manga > svg,
        .bookmark__manga [class*="settings"] { 
            position: absolute !important;
            top: 8px !important;
            right: 8px !important;
            background: rgba(0,0,0,0.6) !important;
            border-radius: 50% !important;
            padding: 4px !important;
            z-index: 10 !important;
            color: #fff !important;
        }
    `);

    // =========================================================================
    // 2. БАЗОВЫЕ ФУНКЦИИ (Архиватор и Уведомления)
    // =========================================================================
    function showToast(html, text, duration = 3000) {
        const t = document.createElement('div');
        t.className = 'mb-toast'; t.innerHTML = html || text;
        document.body.appendChild(t);
        setTimeout(() => t.remove(), duration);
    }

    function makeZip(files) {
        const crcTable = [];
        for (let n = 0; n < 256; n++) { let c = n;
            for (let k = 0; k < 8; k++) c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
            crcTable[n] = c; }
        function getCRC32(data) { let crc = 0 ^ (-1);
            for (let i = 0; i < data.length; i++) crc = (crc >>> 8) ^ crcTable[(crc ^ data[i]) & 0xFF];
            return (crc ^ (-1)) >>> 0; }

        const parts = [];
        const centralDirectory = []; let currentOffset = 0;
        const now = new Date();
        const time = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xFFFF;
        const date = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xFFFF;
        for (const file of files) {
            const nameBytes = new TextEncoder().encode(file.name);
            const dataBytes = file.data; const crc = getCRC32(dataBytes); const size = dataBytes.length;
            const lfh = new Uint8Array(30 + nameBytes.length);
            const view = new DataView(lfh.buffer);
            view.setUint32(0, 0x04034b50, true); view.setUint16(4, 10, true); view.setUint16(10, time, true); view.setUint16(12, date, true);
            view.setUint32(14, crc, true);
            view.setUint32(18, size, true); view.setUint32(22, size, true); view.setUint16(26, nameBytes.length, true);
            lfh.set(nameBytes, 30); parts.push(lfh); parts.push(dataBytes);

            const cdh = new Uint8Array(46 + nameBytes.length);
            const cdView = new DataView(cdh.buffer);
            cdView.setUint32(0, 0x02014b50, true); cdView.setUint16(4, 20, true); cdView.setUint16(6, 10, true); cdView.setUint16(12, time, true);
            cdView.setUint16(14, date, true);
            cdView.setUint32(16, crc, true); cdView.setUint32(20, size, true); cdView.setUint32(24, size, true);
            cdView.setUint16(28, nameBytes.length, true); cdView.setUint32(42, currentOffset, true); cdh.set(nameBytes, 46);
            centralDirectory.push(cdh);
            currentOffset += lfh.length + dataBytes.length;
        }

        const cdBlob = new Blob(centralDirectory);
        const eocd = new Uint8Array(22); const eocdView = new DataView(eocd.buffer);
        eocdView.setUint32(0, 0x06054b50, true); eocdView.setUint16(8, files.length, true); eocdView.setUint16(10, files.length, true);
        eocdView.setUint32(12, cdBlob.size, true); eocdView.setUint32(16, currentOffset, true);
        parts.push(cdBlob); parts.push(eocd); return new Blob(parts, { type: 'application/zip' });
    }

    // =========================================================================
    // 2.5. АПГРЕЙД ОБЛОЖЕК ЗАКЛАДОК (HD обложки)
    // =========================================================================
    function upgradeBookmarkImages() {
        document.querySelectorAll('.bookmark__manga-image').forEach(el => {
            if (el.dataset.mbImgUpgraded) return;
            el.dataset.mbImgUpgraded = '1';

            const inlineStyle = el.getAttribute('style') || '';
            const urlMatch = inlineStyle.match(/url\(\s*["']?([^"')]+)["']?\s*\)/);
            if (!urlMatch) return;

            const originalUrl = urlMatch[1];
            if (!/\/x\d+\//.test(originalUrl)) return;

            const candidates = [
                originalUrl.replace(/^\/x\d+/, ''),
                originalUrl.replace(/\/x\d+\//, '/x800/'),
                originalUrl.replace(/\/x\d+\//, '/x600/'),
                originalUrl.replace(/\/x\d+\//, '/x400/'),
                originalUrl.replace(/\/x\d+\//, '/x300/'),
            ];
            function tryNext(i) {
                if (i >= candidates.length) return;
                const img = new Image();
                img.onload = () => {
                    el.style.backgroundImage = `url("${candidates[i]}")`;
                    console.log('[BMB] ✅', candidates[i], img.naturalWidth + 'x' + img.naturalHeight);
                };
                img.onerror = () => tryNext(i + 1);
                img.src = candidates[i];
            }
            tryNext(0);
        });
    }

    const bookmarkImgObserver = new MutationObserver(mutations => {
        let needsUpgrade = false;
        for (const m of mutations) {
            for (const node of m.addedNodes) {
                if (node.nodeType !== 1) continue;
                if (node.classList?.contains('bookmark__manga-image') || node.querySelector?.('.bookmark__manga-image')) {
                    needsUpgrade = true; break;
                }
            }
            if (needsUpgrade) break;
        }
        if (needsUpgrade) upgradeBookmarkImages();
    });
    bookmarkImgObserver.observe(document.body, { childList: true, subtree: true });

    // =========================================================================
    // 3. УМНЫЕ ЗАКЛАДКИ
    // =========================================================================
    let scrollTimeout;
    window.addEventListener('scroll', () => {
        if (!isChapterPage()) return;
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(() => {
            const images = document.querySelectorAll('.reader-images img, .page-image img, [class*="chapter"] img, .reader img');
            if (!images.length) return;

            let currentIndex = 0;
            for (let i = 0; i < images.length; i++) {
                const rect = images[i].getBoundingClientRect();
                if (rect.top <= window.innerHeight / 2 && rect.bottom >= window.innerHeight / 2) {
                    currentIndex = i; break;
                }
            }
            if (currentIndex > 0) {
                localStorage.setItem('mb_bm_' + location.pathname, currentIndex);
            }
        }, 500);
    });

    function checkSmartBookmark() {
        if (!isChapterPage()) return;
        const savedIndexStr = localStorage.getItem('mb_bm_' + location.pathname);
        if (!savedIndexStr) return;
        
        const savedIndex = parseInt(savedIndexStr, 10);
        const oldToast = document.getElementById('mb-bookmark-toast');
        if (oldToast) oldToast.remove();
        if (savedIndex && savedIndex > 0) {
            const toast = document.createElement('div');
            toast.id = 'mb-bookmark-toast';
            toast.innerHTML = `
                📍 Вы остановились на ${savedIndex} стр.
                <span class="mb-bm-link">Вернуться</span>
                <span class="mb-bm-close">✖</span>
            `;
            toast.querySelector('.mb-bm-link').addEventListener('click', () => {
                const images = document.querySelectorAll('.reader-images img, .page-image img, [class*="chapter"] img, .reader img');
                if (images[savedIndex]) {
                    images[savedIndex].scrollIntoView({ behavior: 'smooth', block: 'start' });
                    toast.remove();
                } else {
                    showToast(null, '❌ Страница еще не подгрузилась, листайте чуть медленнее...', 3000);
                }
            });
            toast.querySelector('.mb-bm-close').addEventListener('click', () => toast.remove());
            document.body.appendChild(toast);
            setTimeout(() => { if (toast.parentNode) toast.remove(); }, 10000);
        }
    }

    // =========================================================================
    // 4. СКАЧИВАНИЕ (В читалке и из списка)
    // =========================================================================
    async function downloadChapterBackground(url, chapterNameFallback, btnElement) {
        showToast(null, `⏳ Запрос главы...`, 2000);
        GM_xmlhttpRequest({
            method: 'GET', url: url,
            onload: async function(res) {
                if (res.status === 200) {
                    const doc = new DOMParser().parseFromString(res.responseText, "text/html");
                    const urls = [];
  
                    doc.querySelectorAll('.reader-images img, .page-image img, [class*="chapter"] img, .reader img').forEach(img => {
                        const src = img.getAttribute('data-src') || img.src;
                        if (src && !urls.includes(src) && !src.includes('avatar') && !src.includes('logo')) urls.push(src);
                    });

                    if (!urls.length) {
                        showToast(null, '❌ Страницы в этой главе не найдены!', 3000);
                        btnElement.classList.remove('loading'); btnElement.innerHTML = '📥'; return;
                    }

                    let downloadedFiles = [], done = 0, errors = 0; btnElement.innerHTML = `0/${urls.length}`;
                    await Promise.all(urls.map((imgUrl, index) => new Promise(resolve => {
                        GM_xmlhttpRequest({
                            method: 'GET', url: imgUrl, responseType: 'arraybuffer', headers: { Referer: location.origin },
                            onload: function (imgRes) {
                                if (imgRes.status === 200 && imgRes.response) {
                                    try {
                                        const ext = imgUrl.split('.').pop().split('?')[0] || 'jpg';
                                        downloadedFiles.push({ name: String(index + 1).padStart(3, '0') + '.' + ext, data: new Uint8Array(imgRes.response), index: index });
                                        done++;
                                     } catch(e) { errors++; }
                                } else errors++;
                                btnElement.innerHTML = `${done + errors}/${urls.length}`; resolve();
                            },
                            onerror: () => { errors++; btnElement.innerHTML = `${done + errors}/${urls.length}`; resolve(); },
                            ontimeout: () => { errors++; btnElement.innerHTML = `${done + errors}/${urls.length}`; resolve(); }
                        });
                    })));

                    if (done === 0) { 
                        showToast(null, '❌ Ошибка загрузки страниц.', 3000);
                        btnElement.classList.remove('loading'); btnElement.innerHTML = '📥'; return; 
                    }
                    
                    btnElement.innerHTML = '📦';
                    let safeName = chapterNameFallback.replace(/[^\w\sа-яА-ЯёЁ]/g, '').trim().slice(0, 40) || 'chapter';
                    let mangaTitle = document.title.split('-')[0].trim().replace(/[^\w\sа-яА-ЯёЁ]/g, '');
                    setTimeout(() => {
                        try {
                            downloadedFiles.sort((a, b) => a.index - b.index);
                            const a = document.createElement('a');
                            a.href = URL.createObjectURL(makeZip(downloadedFiles));
                            a.download = `${mangaTitle}_${safeName}`.replace(/\s+/g, '_') + '.zip';
                            a.click(); URL.revokeObjectURL(a.href);
                            showToast(`<span class="mbd-ok">✅ ${safeName} скачана!</span>`, 4000);
                        } catch (err) { showToast(null, '❌ Ошибка архива.', 4000); } 
                        finally { btnElement.classList.remove('loading'); btnElement.innerHTML = '📥'; }
                    }, 50);
                } else {
                    showToast(null, '❌ Ошибка доступа.', 3000);
                    btnElement.classList.remove('loading'); btnElement.innerHTML = '📥';
                }
            }
        });
    }

    function injectListDownloadButtons() {
        if (!location.pathname.includes('/manga/') || isChapterPage()) return;
        document.querySelectorAll('a[href*="/manga/"]:not(.mb-dl-injected)').forEach(link => {
            const href = link.getAttribute('href'); if (!href) return;
            const text = link.innerText.toLowerCase();
            
            if (/\/\d+(?:[._]\d+)?(?:\/\d+(?:[._]\d+)?)?\/?$/.test(href) && (text.includes('том') || text.includes('глава') || text.includes('прочитано'))) {
                const rightPart = Array.from(link.children).find(child => /\d{2}\.\d{2}\.\d{4}/.test(child.innerText) || child.innerText.includes('K') || child.innerText.toLowerCase().includes('прочитано'));
                if (!rightPart) return;
                
                link.classList.add('mb-dl-injected');
                const dlBtn = document.createElement('button');
                dlBtn.innerHTML = '📥'; dlBtn.className = 'mb-list-dl-btn'; dlBtn.title = 'Скачать главу';
      
                dlBtn.addEventListener('click', (e) => {
                    e.preventDefault(); e.stopPropagation();
                    if (dlBtn.classList.contains('loading')) return;
                    dlBtn.classList.add('loading'); dlBtn.innerHTML = '⏳';
                    
                    const chapNameMatch = link.innerText.match(/Том\s*\d+\s*Глава\s*\d+(?:\.\d+)?/i) || link.innerText.match(/Глава\s*\d+(?:\.\d+)?/i);
                    let chapName = 'Chapter';
                    if (chapNameMatch) { chapName = chapNameMatch[0]; } 
                    else {
                        const parts = href.split('/').filter(Boolean);
                        chapName = !isNaN(parts[parts.length - 2]) ? `Том ${parts[parts.length - 2]} Глава ${parts[parts.length - 1]}` : `Глава ${parts[parts.length - 1]}`;
                    }
                    downloadChapterBackground(link.href, chapName, dlBtn);
                });
                rightPart.appendChild(dlBtn);
            }
        });
    }

    // =========================================================================
    // 5. ОСТАЛЬНОЙ ИНТЕРФЕЙС
    // =========================================================================
    function injectPromoLink() {
        const subMenu = document.querySelector('.header__link-sub');
        if (subMenu && !subMenu.querySelector('a[href*="promo-code"]')) {
            const a = document.createElement('a');
            a.href = 'https://mangabuff.ru/promo-code'; a.className = 'mb-promo-link'; a.innerText = 'Промокоды';
            const sLink = subMenu.querySelector('a');
            if (sLink) a.className += ' ' + sLink.className;
            subMenu.appendChild(a);
        }
    }

    const emojis = ['😀','😂','🤣','😊','😍','😘','😜','🤫','🤔','😎','🙄','🤡','💩','🔥','✨','💯','👍','👎','❤️','💔','😭','😡','😱','💀','👽','🤖','👑','👀','💬','🐾','🐈','🦊','🍕','🎮','🎲'];
    function initEmojiPicker() {
        // Изменен селектор, чтобы находить поле ввода и в режиме обычного чата, и в ответах
        const input = document.querySelector('input[placeholder*="сообщение"], textarea[placeholder*="сообщение"], input[placeholder*="твет"], textarea[placeholder*="твет"]');
        if (!input || input.dataset.emojiInit) return; input.dataset.emojiInit = "true";
        const dBtn = input.parentElement.querySelector('button, div[class*="theme"]');
        const eBtn = document.createElement('button'); eBtn.type = 'button';
        eBtn.className = 'custom-emoji-btn'; eBtn.innerText = '😀';
        const picker = document.createElement('div'); picker.className = 'custom-emoji-picker'; picker.style.display = 'none';
        input.parentElement.style.position = 'relative';
        emojis.forEach(e => {
            const s = document.createElement('span'); s.className = 'custom-emoji-item'; s.innerText = e;
            s.addEventListener('click', ev => {
                ev.stopPropagation(); const start = input.selectionStart, end = input.selectionEnd, val = input.value;
                input.value = val.substring(0, start) + e + val.substring(end); input.focus();
                input.selectionStart = input.selectionEnd = start + e.length; input.dispatchEvent(new Event('input', { bubbles: true }));
            });
            picker.appendChild(s);
        });
        if (dBtn) dBtn.after(eBtn); else input.before(eBtn);
        input.parentElement.appendChild(picker);
        eBtn.addEventListener('click', ev => { ev.stopPropagation(); picker.style.display = picker.style.display === 'none' ? 'grid' : 'none'; });
        document.addEventListener('click', () => { picker.style.display = 'none'; });
    }

    function manageReaderUI() {
        if (isChapterPage()) {
            document.body.classList.add('mb-reader-mode');
            setTimeout(checkSmartBookmark, 1000); 

            if (!document.getElementById('mb-download-btn')) {
                const btn = document.createElement('button');
                btn.id = 'mb-download-btn'; btn.innerHTML = '⬇ Скачать главу'; document.body.appendChild(btn);
                btn.addEventListener('click', async () => {
                    if (btn.classList.contains('loading')) return;
                    const urls = []; 
                    document.querySelectorAll('.reader-images img, .page-image img, [class*="chapter"] img, .reader img').forEach(img => { 
                        const src = img.getAttribute('data-src') || img.src; if (src && !urls.includes(src) && !src.includes('avatar')) urls.push(src); 
                    });
                    if (!urls.length) { showToast(null, '❌ Картинки не найдены!', 4000); return; }

                    btn.classList.add('loading'); let files = [], done = 0, errs = 0; btn.innerHTML = `⏳ 0/${urls.length}`;
                    await Promise.all(urls.map((url, i) => new Promise(res => {
                        GM_xmlhttpRequest({
                            method: 'GET', url: url, responseType: 'arraybuffer',
                            onload: r => { if (r.status===200 && r.response) { try { files.push({ name: String(i+1).padStart(3, '0')+'.'+(url.split('.').pop().split('?')[0]||'jpg'), data: new Uint8Array(r.response), index: i });
                                done++; } catch(e){errs++;} } else errs++; btn.innerHTML=`⏳ ${done+errs}/${urls.length}`; res(); },
                            onerror: () => { errs++; btn.innerHTML=`⏳ ${done+errs}/${urls.length}`; res(); }
                        });
                    })));

                    if (done === 0) { btn.classList.remove('loading'); btn.innerHTML = '⬇ Скачать главу'; return; }
                    btn.innerHTML = '📦 Упаковываю...';
                    const m = location.pathname.match(/chapter[-_]?(\d+(?:[._]\d+)?)/i) || location.pathname.match(/\/(\d+(?:[._]\d+)?)\/?$/);
                    const chap = m ? 'ch' + m[1] : 'chapter';
                    const title = document.title.replace(/[^\w\sа-яА-ЯёЁ]/g, '').slice(0, 40);
                    setTimeout(() => {
                        try { files.sort((a, b) => a.index - b.index); const a = document.createElement('a'); a.href = URL.createObjectURL(makeZip(files)); a.download = title+'_'+chap+'.zip'; a.click(); showToast(`<span class="mbd-ok">✅ Скачано: ${done} стр.</span>`, 5000); } 
                        catch (e) { showToast(null, '❌ Ошибка архива.'); } finally { btn.classList.remove('loading'); btn.innerHTML = '⬇ Скачать главу'; }
                    }, 50);
                });
            }
        } else {
            document.body.classList.remove('mb-reader-mode');
            const btn = document.getElementById('mb-download-btn'); if (btn) btn.remove();
        }
    }

    // =========================================================================
    // НАБЛЮДАТЕЛИ (SPA Навигация и Хоткеи)
    // =========================================================================
    document.addEventListener('keydown', e => {
        if (!isChapterPage() || ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return;
        let dir = null; if (['ArrowLeft', 'a', 'A', 'ф', 'Ф'].includes(e.key)) dir = 'prev'; if (['ArrowRight', 'd', 'D', 'в', 'В'].includes(e.key)) dir = 'next';
        if (!dir) return;
        for (let el of document.querySelectorAll('a, button')) {
            const text = el.innerText?.toLowerCase() || ''; const href = el.getAttribute('href') || '';
            if (dir === 'next' && (text.includes('след') || href.includes('next') || el.classList.contains('next'))) { el.click(); break; }
            if (dir === 'prev' && (text.includes('пред') || text.includes('прош') || href.includes('prev') || el.classList.contains('prev'))) { el.click(); break; }
        }
    });

    let lastUrl = location.href;
    const globalObserver = new MutationObserver(() => {
        injectPromoLink(); initEmojiPicker(); injectListDownloadButtons();
        if (location.href !== lastUrl) { lastUrl = location.href; manageReaderUI(); }
    });
    globalObserver.observe(document.body, { childList: true, subtree: true });
    manageReaderUI(); setTimeout(initEmojiPicker, 1000); upgradeBookmarkImages();

})();