Arhivach Blacklist

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();