Arhivach Blacklist

Скрывает треды из Чёрного списка

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Arhivach Blacklist
// @namespace    Arhivach Blacklist
// @version      1.0
// @description  Скрывает треды из Чёрного списка
// @author       glauthentica
// @match        https://arhivach.vc/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_addValueChangeListener
// @run-at       document-body
// @license      MIT
// @homepageURL      https://boosty.to/glauthentica
// @contributionURL  https://boosty.to/glauthentica
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAdVBMVEUAAABJCzE/CipECi5LCzJFCi6rG3OoGnE5CSZQDDY4CCWjGW6UF2MiBRcKLk0XZ6sLMVEZb7gJK0gPQm4WZKcSUYYWZagEEx8IJT0LRzIZoXETfVcKRjENUzoINiYMUjoKRTAHMSIQakoQa0sQaUoQbUwRbU1wXogmAAAAAXRSTlMAQObYZgAAAGpJREFUeAF9jAsKAkEMQ9sko1a8//X2GLt8Bu18KCLgozTwIIH9kMLBDTSFRO57T5HfnOSIISYkvTai936d55URa/RVrAq+qA0HamMFoBL3RCRG/tnIqwpHSMpYlfaIuLWmiKdPcdA2OMw+nQcPhNt65VYAAAAASUVORK5CYII=
// ==/UserScript==

(function() {
    'use strict';

    // --- Ключ хранения ---
    const HIDDEN_THREADS_KEY = 'arhivach_hidden_threads_v8_titles';
    let hiddenThreads = GM_getValue(HIDDEN_THREADS_KEY, []) || [];

    // --- Стили ---
    GM_addStyle(`
        /* Общие стили для модального окна и иконки скрытия */
        .ath-hide-btn { cursor: pointer; margin-left: 4px; display: inline-block; vertical-align: middle; font-size: 1.2em; line-height: 1; }
        .ath-thread-page-hide-btn { cursor: pointer; margin-left: 5px; vertical-align: middle; } /* Стиль для кнопки в треде */

        #hidden-threads-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.75); z-index: 10000; display: none; justify-content: center; align-items: center; }
        #hidden-threads-modal-content { background-color: #2c2f33; color: #eee; padding: 20px; border-radius: 8px; width: 90%; max-width: 800px; max-height: 80vh; overflow-y: auto; border: 1px solid #4f545c; display: flex; flex-direction: column; }
        #hidden-threads-modal-content h2 { margin-top: 0; border-bottom: 1px solid #4f545c; padding-bottom: 10px; }
        #hidden-threads-list { flex-grow: 1; overflow-y: auto; margin-bottom: 15px; }
        #hidden-threads-list div { padding: 2px 8px; border-bottom: 1px solid #3a3d40; display: flex; justify-content: space-between; align-items: center; }
        #hidden-threads-list div a { flex-grow: 1; margin-right: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .ath-restore-btn { background-color: #4caf50; color: white; border: none; padding: 3px 8px; border-radius: 4px; cursor: pointer; flex-shrink: 0; font-size: 0.9em; }
        #hidden-threads-modal-buttons { margin-top: 10px; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
        #hidden-threads-modal-buttons button { border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; color: white; font-weight: bold; }
        #hidden-threads-modal-backup-controls { border-top: 1px solid #4f545c; padding-top: 15px; margin-top: 15px; display: flex; gap: 10px; justify-content: flex-start; }
        #ath-backup-btn { background-color: #0288d1; }
        #ath-restore-btn { background-color: #512da8; }
        #ath-restore-all-btn { background-color: #d32f2f; }
        #ath-modal-close-btn { background-color: #616161; }

        /* Стилизация кнопки в главном меню */
        #hidden-threads-manager-li a {
            padding: 10px 15px;
            font-weight: normal !important;
            display: flex !important;
            align-items: center !important;
            gap: 0.5em !important;
        }

        /* Стилизация бейджа */
        .ath-badge {
            padding: 1px 6px;
            font-size: 10px;
            font-weight: bold;
            line-height: 1.4;
            border-radius: 8px;
            text-shadow: none;
        }
        body:not(.dark) .ath-badge { background-color: #777777; color: white; }
        body.dark .ath-badge { background-color: #c6c6c6; color: #333333; }
    `);

    // --- Функции для работы с хранилищем ---
    const saveHiddenThreads = () => GM_setValue(HIDDEN_THREADS_KEY, hiddenThreads);
    const loadHiddenThreads = () => { hiddenThreads = GM_getValue(HIDDEN_THREADS_KEY, []) || []; };

    // --- Обработка треда В СПИСКЕ ---
    function processThreadRow(threadRow) {
        if (threadRow.dataset.hiderProcessed) return;
        threadRow.dataset.hiderProcessed = 'true';
        const threadId = threadRow.id.replace('thread_row_', '');
        if (!threadId) return;
        if (hiddenThreads.some(thread => thread.id === threadId)) {
            threadRow.style.display = 'none';
        }
        const controlCell = threadRow.querySelector('.thread_control nobr');
        if (controlCell) {
            const hideLink = document.createElement('a');
            hideLink.href = '#';
            hideLink.className = 'ath-hide-btn';
            hideLink.title = 'Скрыть этот тред';
            hideLink.innerHTML = '<i class="icon-eye-close"></i>';
            hideLink.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                if (!hiddenThreads.some(thread => thread.id === threadId)) {
                    let threadTitle = 'Без заголовка';
                    const titleElement = threadRow.querySelector('div.thread_text a b');
                    if (titleElement && titleElement.textContent.trim()) {
                        threadTitle = titleElement.textContent.trim();
                    } else {
                        const linkElement = threadRow.querySelector('div.thread_text a');
                        if (linkElement && linkElement.textContent.trim()) {
                            const fullText = linkElement.textContent.trim().replace(/\s+/g, ' ');
                            threadTitle = fullText.substring(0, 70) + (fullText.length > 70 ? '...' : '');
                        }
                    }
                    hiddenThreads.unshift({ id: threadId, title: threadTitle });
                    saveHiddenThreads();
                    threadRow.style.display = 'none';
                    updateManagementButton();
                }
            });
            controlCell.appendChild(hideLink);
        }
    }

    // --- Функция: Обработка кнопки НА СТРАНИЦЕ ТРЕДА ---
    function addOrUpdateThreadPageButton() {
        const header = document.getElementById('thread_header');
        if (!header) return;

        const threadIdMatch = window.location.pathname.match(/\/thread\/(\d+)/);
        if (!threadIdMatch) return;
        const threadId = threadIdMatch[1];

        const container = header.querySelector('.span2 nobr');
        if (!container) return;

        const oldBtn = container.querySelector('.ath-thread-page-hide-btn');
        if (oldBtn) oldBtn.remove();

        const isHidden = hiddenThreads.some(t => t.id === threadId);
        const hideLink = document.createElement('a');
        hideLink.href = '#';
        hideLink.className = 'ath-thread-page-hide-btn';

        if (isHidden) {
            hideLink.title = 'Вернуть этот тред из Чёрного списка';
            hideLink.innerHTML = '<i class="icon-ban-circle"></i>';
            // ИЗМЕНЕНИЕ: Строка, задающая красный цвет, удалена.
            hideLink.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                hiddenThreads = hiddenThreads.filter(thread => thread.id !== threadId);
                saveHiddenThreads();
                updateManagementButton();
                addOrUpdateThreadPageButton();
            });
        } else {
            hideLink.title = 'Скрыть этот тред';
            hideLink.innerHTML = '<i class="icon-eye-close"></i>';
            hideLink.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                const threadTitle = document.title.replace(/ - Arhivach$/, '').trim() || `Тред #${threadId}`;
                if (!hiddenThreads.some(thread => thread.id === threadId)) {
                    hiddenThreads.unshift({ id: threadId, title: threadTitle });
                    saveHiddenThreads();
                    updateManagementButton();
                    addOrUpdateThreadPageButton();
                }
            });
        }

        const favStar = container.querySelector('a[id^="infav"]');
        if (favStar) {
            favStar.parentNode.insertBefore(hideLink, favStar.nextSibling);
        } else {
            container.appendChild(hideLink);
        }
    }


    // --- Функции бэкапа и восстановления ---
    function handleBackup() {
        if (hiddenThreads.length === 0) {
            alert('Нет скрытых тредов для экспорта.');
            return;
        }
        const dataStr = JSON.stringify(hiddenThreads, null, 2);
        const blob = new Blob([dataStr], {type: "application/json"});
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        const date = new Date().toISOString().slice(0, 10);
        a.href = url;
        a.download = `Arhivach Blacklist (${date}).json`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }
    function handleRestoreClick() { document.getElementById('ath-restore-input').click(); }
    function handleFileSelect(event) {
        const file = event.target.files[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = function(e) {
            try {
                const data = JSON.parse(e.target.result);
                if (!Array.isArray(data) || (data.length > 0 && (typeof data[0].id === 'undefined' || typeof data[0].title === 'undefined'))) {
                   throw new Error('Неверный формат файла. Ожидается массив объектов с полями "id" и "title".');
                }
                if (confirm(`Вы уверены, что хотите импортировать ${data.length} записей из файла? \n\nВНИМАНИЕ: Это действие ЗАМЕНИТ ваш текущий список скрытых тредов.`)) {
                    hiddenThreads = data;
                    saveHiddenThreads();
                    alert(`Импорт успешно завершен. Будет перезагружена страница.`);
                    window.location.reload();
                }
            } catch (error) {
                alert('Ошибка при чтении или обработке файла: ' + error.message);
            }
        };
        reader.readAsText(file);
        event.target.value = '';
    }

    // --- UI управления ---
    function handleEscKey(event) { if (event.key === 'Escape') { hideManagementModal(); } }
    function hideManagementModal() {
        const overlay = document.getElementById('hidden-threads-modal-overlay');
        if (overlay) {
            overlay.style.display = 'none';
            document.removeEventListener('keydown', handleEscKey);
        }
    }
    function createManagementUI() {
        if (document.getElementById('hidden-threads-manager-btn')) return;
        const topMenu = document.querySelector('ul.nav');
        if (!topMenu) return;
        const managementLi = document.createElement('li');
        managementLi.id = 'hidden-threads-manager-li';
        topMenu.insertBefore(managementLi, topMenu.firstChild);
        managementLi.addEventListener('click', (e) => { e.preventDefault(); showManagementModal(); });
        updateManagementButton();
        createModal();
    }
    function updateManagementButton() {
        const li = document.getElementById('hidden-threads-manager-li');
        if (li) {
            const count = hiddenThreads.length;
            const badgeHtml = count > 0 ? `<span class="ath-badge">${count}</span>` : '';
            li.innerHTML = `<a href="#" id="hidden-threads-manager-btn"><i class="icon-eye-close"></i><span>Скрытые</span>${badgeHtml}</a>`;
        }
    }
    function createModal() {
        if (document.getElementById('hidden-threads-modal-overlay')) return;
        const overlay = document.createElement('div');
        overlay.id = 'hidden-threads-modal-overlay';
        overlay.innerHTML = `
            <div id="hidden-threads-modal-content">
                <h2>Управление скрытыми тредами</h2>
                <div id="hidden-threads-list"></div>
                <div id="hidden-threads-modal-backup-controls">
                    <button id="ath-backup-btn">Экспорт в JSON</button>
                    <button id="ath-restore-btn">Импорт из JSON</button>
                    <input type="file" id="ath-restore-input" accept=".json" style="display: none;">
                </div>
                <div id="hidden-threads-modal-buttons">
                    <button id="ath-restore-all-btn">Восстановить все и перезагрузить</button>
                    <button id="ath-modal-close-btn">Закрыть</button>
                </div>
            </div>`;
        document.body.appendChild(overlay);

        overlay.addEventListener('click', (e) => {
            if (e.target.id === 'hidden-threads-modal-overlay' || e.target.id === 'ath-modal-close-btn') {
                hideManagementModal();
            }
        });
        document.getElementById('ath-restore-all-btn').addEventListener('click', () => {
            if (confirm('Вы уверены? Это действие восстановит все скрытые треды и перезагрузит страницу.')) {
                GM_setValue(HIDDEN_THREADS_KEY, []);
                window.location.reload();
            }
        });
        document.getElementById('ath-backup-btn').addEventListener('click', handleBackup);
        document.getElementById('ath-restore-btn').addEventListener('click', handleRestoreClick);
        document.getElementById('ath-restore-input').addEventListener('change', handleFileSelect);
    }
    function showManagementModal() {
        loadHiddenThreads();
        const listContainer = document.getElementById('hidden-threads-list');
        listContainer.innerHTML = hiddenThreads.length === 0 ? '<div>Нет скрытых тредов.</div>' : '';

        hiddenThreads.forEach(thread => {
            const item = document.createElement('div');
            item.innerHTML = `
                <a href="/thread/${thread.id}/" target="_blank" title="${thread.title}">Тред #${thread.id} (${thread.title})</a>
                <button class="ath-restore-btn" data-id="${thread.id}">Восстановить</button>
            `;
            listContainer.appendChild(item);
        });

        listContainer.querySelectorAll('.ath-restore-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const idToRestore = e.target.dataset.id;
                hiddenThreads = hiddenThreads.filter(thread => thread.id !== idToRestore);
                saveHiddenThreads();
                const threadRowToShow = document.getElementById('thread_row_' + idToRestore);
                if (threadRowToShow) { threadRowToShow.style.display = ''; }
                updateManagementButton();
                showManagementModal();
            });
        });

        document.getElementById('hidden-threads-modal-overlay').style.display = 'flex';
        document.addEventListener('keydown', handleEscKey);
    }

    // --- ИНИЦИАЛИЗАЦИЯ И НАБЛЮДЕНИЕ ---
    function initialize() {
        loadHiddenThreads();
        createManagementUI();

        if (window.location.pathname.startsWith('/thread/')) {
            addOrUpdateThreadPageButton();
        } else {
            document.querySelectorAll('tr[id^="thread_row_"]').forEach(processThreadRow);
        }
    }

    const threadObserver = new MutationObserver((mutations) => {
        if (!document.getElementById('hidden-threads-manager-btn')) { createManagementUI(); }
        for (const mutation of mutations) {
            if (mutation.addedNodes.length) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches('tr[id^="thread_row_"]')) { processThreadRow(node); }
                        node.querySelectorAll('tr[id^="thread_row_"]').forEach(processThreadRow);
                    }
                }
            }
        }
        if (window.location.pathname.startsWith('/thread/')) {
            const header = document.getElementById('thread_header');
            if (header && !header.querySelector('.ath-thread-page-hide-btn')) {
                 addOrUpdateThreadPageButton();
            }
        }
    });
    threadObserver.observe(document.body, { childList: true, subtree: true });

    GM_addValueChangeListener(HIDDEN_THREADS_KEY, (name, oldValue, newValue, remote) => {
        if (remote) {
            hiddenThreads = newValue || [];
            updateManagementButton();

            if (window.location.pathname.startsWith('/thread/')) {
                addOrUpdateThreadPageButton();
            } else {
                document.querySelectorAll('tr[id^="thread_row_"]').forEach(row => {
                    const threadId = row.id.replace('thread_row_', '');
                    const isHidden = hiddenThreads.some(thread => thread.id === threadId);
                    row.style.display = isHidden ? 'none' : '';
                });
            }
        }
    });

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();