Скрывает треды из Чёрного списка
// ==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();
}
})();