Habr Content Filter

Фильтрация контента на Хабре: скрытие статей, блокировка авторов и корпоративных блогов, фильтр тегов, очистка ленты. Помогает скрыть ненужные посты и настроить персональную ленту Habr.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Habr Content Filter
// @name:ru      Фильтр контента Хабра (Habr Content Filter)
// @namespace    https://github.com/aifixed/userscripts
// @version      2.5.1
// @description  Фильтрация контента на Хабре: скрытие статей, блокировка авторов и корпоративных блогов, фильтр тегов, очистка ленты. Помогает скрыть ненужные посты и настроить персональную ленту Habr.
// @description:ru Фильтрация контента на Хабре: скрытие статей, блокировка авторов и корпоративных блогов, фильтр тегов, очистка ленты. Помогает скрыть ненужные посты и настроить персональную ленту Habr.
// @author       AiFixed
// @license      MIT
// @homepage     https://aifixed.github.io/userscripts/
// @homepageURL  https://aifixed.github.io/userscripts/
// @supportURL   https://github.com/aifixed/userscripts/issues
//
// --- Матчи (по умолчанию покрывают всё необходимое) ---
// @match        https://habr.com/*
// @match        https://habr.com/ru/*
// @match        https://habr.com/ru/articles/*
// @match        https://habr.com/ru/feed/*
// @match        https://habr.com/ru/hubs/*
// @match        https://habr.com/ru/users/*
// @match        https://habr.com/ru/companies/*
//
// --- Прочее ---
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ======================
    // STORAGE MANAGER
    // (JS-логика без изменений, она оптимальна)
    // ======================
    class StorageManager {
        constructor() {
            this.data = this.load();
        }

        load() {
            const defaultData = {
                tags: {},
                authors: {},
                commenters: {},
                settings: {
                    showStats: true,
                    animateHiding: true,
                    showPreview: true,
                    focusMode: false
                },
                stats: {
                    totalHidden: 0,
                    lastUpdate: Date.now()
                }
            };

            try {
                const saved = localStorage.getItem('habrFilterData');
                return saved ? { ...defaultData, ...JSON.parse(saved) } : defaultData;
            } catch (e) {
                console.error('Failed to load data:', e);
                return defaultData;
            }
        }

        save() {
            try {
                this.data.stats.lastUpdate = Date.now();
                localStorage.setItem('habrFilterData', JSON.stringify(this.data));
            } catch (e) {
                console.error('Failed to save data:', e);
            }
        }

        addBlock(type, value, duration = 'permanent') {
            if (!this.data[type]) this.data[type] = {};

            const existing = this.data[type][value] || { hidden: 0, blocked: false };
            this.data[type][value] = {
                ...existing,
                blocked: true,
                blockedAt: Date.now(),
                duration: duration
            };

            this.save();
        }

        removeBlock(type, value) {
            if (this.data[type] && this.data[type][value]) {
                this.data[type][value].blocked = false;
                this.save();
            }
        }

        incrementHidden(type, value) {
            if (!this.data[type][value]) {
                this.data[type][value] = { hidden: 0, blocked: false };
            }
            this.data[type][value].hidden++;
            this.data.stats.totalHidden++;
            this.save();
        }

        isBlocked(type, value) {
            const item = this.data[type][value];
            if (!item || !item.blocked) return false;

            if (item.duration !== 'permanent') {
                const duration = {
                    'day': 86400000,
                    'week': 604800000,
                    'month': 2592000000
                }[item.duration];

                if (Date.now() - item.blockedAt > duration) {
                    this.removeBlock(type, value);
                    return false;
                }
            }
            return true;
        }

        getStats(type, value) {
            return this.data[type][value] || { hidden: 0, blocked: false };
        }

        export() {
            return JSON.stringify(this.data, null, 2);
        }

        import(jsonString) {
            try {
                this.data = JSON.parse(jsonString);
                this.save();
                return true;
            } catch (e) {
                console.error('Import failed:', e);
                return false;
            }
        }
    }

    // ======================
    // UI COMPONENTS
    // (JS-логика без изменений, HTML-экранизация на месте)
    // ======================
    class UIComponents {
        
        static escapeHTML(str) {
            if (!str) return '';
            return str.replace(/[&<>"']/g, function(m) {
                return {
                    '&': '&amp;',
                    '<': '&lt;',
                    '>': '&gt;',
                    '"': '&quot;',
                    "'": '&#39;'
                }[m];
            });
        }

        static createContextMenu(type, value, stats) {
            const isBlocked = stats.blocked;
            const menu = document.createElement('div');
            menu.className = 'hf-context-menu';
            menu.innerHTML = `
                <div class="hf-menu-header">
                    <strong>${this.escapeHTML(value)}</strong>
                    <button class="hf-menu-close" title="Закрыть">×</button>
                </div>
                <div class="hf-menu-content">
                    ${!isBlocked ? `
                        <button class="hf-menu-item hf-block-action" data-duration="permanent">
                            <span class="hf-menu-icon">⛔</span>
                            Заблокировать навсегда
                        </button>
                        <button class="hf-menu-item hf-block-action" data-duration="week">
                            <span class="hf-menu-icon">📅</span>
                            Заблокировать на неделю
                        </button>
                        <button class="hf-menu-item hf-block-action" data-duration="day">
                            <span class="hf-menu-icon">⏰</span>
                            Заблокировать на день
                        </button>
                    ` : `
                        <button class="hf-menu-item hf-unblock-action">
                            <span class="hf-menu-icon">✅</span>
                            Разблокировать
                        </button>
                    `}
                    <div class="hf-menu-divider"></div>
                    <div class="hf-menu-stats">
                        <div class="hf-stat-item">
                            <span class="hf-stat-label">Скрыто:</span>
                            <span class="hf-stat-value">${stats.hidden || 0}</span>
                        </div>
                        ${stats.blockedAt ? `
                            <div class="hf-stat-item">
                                <span class="hf-stat-label">Заблокировано:</span>
                                <span class="hf-stat-value">${this.formatDate(stats.blockedAt)}</span>
                            </div>
                        ` : ''}
                    </div>
                </div>
            `;
            return menu;
        }

        static createControlPanel(storage) {
            const panel = document.createElement('div');
            panel.id = 'hf-control-panel';
            panel.className = 'hf-panel hf-panel-collapsed';

            const blockedTags = Object.entries(storage.data.tags).filter(([_, v]) => v.blocked);
            const blockedAuthors = Object.entries(storage.data.authors).filter(([_, v]) => v.blocked);

            const inactiveTags = Object.entries(storage.data.tags).filter(([_, v]) => !v.blocked && (v.hidden || 0) > 0);
            const inactiveAuthors = Object.entries(storage.data.authors).filter(([_, v]) => !v.blocked && (v.hidden || 0) > 0);

            const renderBlockedItem = (type, value, stats, active = true) => `
                <div class="hf-blocked-item ${active ? '' : 'hf-blocked-item-inactive'}" data-type="${type}" data-value="${this.escapeHTML(value)}">
                    <span class="hf-blocked-name">${this.escapeHTML(value)}</span>
                    <span class="hf-blocked-count">${stats.hidden || 0}</span>
                    <button class="${active ? 'hf-unblock-btn' : 'hf-reactivate-btn'}" title="${active ? 'Разблокировать' : 'Снова блокировать'}">
                        ${active ? '✓' : '↺'}
                    </button>
                </div>
            `;

            panel.innerHTML = `
                <button class="hf-panel-toggle" title="Настройки фильтра">
                    <span class="hf-panel-icon">⚙️</span>
                    <span class="hf-panel-badge">${blockedTags.length + blockedAuthors.length}</span>
                </button>
                <div class="hf-panel-content">
                    <div class="hf-panel-header">
                        <h3>Фильтрация контента</h3>
                        <button class="hf-panel-close">×</button>
                    </div>

                    <div class="hf-panel-body">
                        <div class="hf-stats-summary">
                            <div class="hf-stat-card">
                                <div class="hf-stat-number">${storage.data.stats.totalHidden}</div>
                                <div class="hf-stat-label">Всего скрыто</div>
                            </div>
                        </div>

                        <div class="hf-section">
                            <h4>🏷️ Активные заблокированные теги (${blockedTags.length})</h4>
                            <div class="hf-blocked-list" id="hf-blocked-tags">
                                ${blockedTags.length ? blockedTags.map(([tag, stats]) => renderBlockedItem('tags', tag, stats, true)).join('') : '<div class="hf-empty">Нет активных заблокированных тегов</div>'}
                            </div>
                        </div>

                        <div class="hf-section">
                            <h4>✍️ Активные заблокированные авторы (${blockedAuthors.length})</h4>
                            <div class="hf-blocked-list" id="hf-blocked-authors">
                                ${blockedAuthors.length ? blockedAuthors.map(([author, stats]) => renderBlockedItem('authors', author, stats, true)).join('') : '<div class="hf-empty">Нет активных заблокированных авторов</div>'}
                            </div>
                        </div>

                        <div class="hf-section">
                            <h4>🕊 Ранее скрывавшиеся теги (${inactiveTags.length})</h4>
                            <div class="hf-blocked-list" id="hf-inactive-tags">
                                ${inactiveTags.length ? inactiveTags.map(([tag, stats]) => renderBlockedItem('tags', tag, stats, false)).join('') : '<div class="hf-empty">Нет ранее скрывавшихся тегов</div>'}
                            </div>
                        </div>

                        <div class="hf-section">
                            <h4>🕊 Ранее скрывавшиеся авторы (${inactiveAuthors.length})</h4>
                            <div class="hf-blocked-list" id="hf-inactive-authors">
                                ${inactiveAuthors.length ? inactiveAuthors.map(([author, stats]) => renderBlockedItem('authors', author, stats, false)).join('') : '<div class="hf-empty">Нет ранее скрывавшихся авторов</div>'}
                            </div>
                        </div>

                        <div class="hf-tip">
                            💡 <strong>Совет:</strong> Shift+Click по тегу или автору для быстрой блокировки
                        </div>
                    </div>
                </div>
            `;
            return panel;
        }

        static createNotification(message, type = 'info') {
            const notification = document.createElement('div');
            notification.className = `hf-notification hf-notification-${type}`;
            notification.innerHTML = `
                <div class="hf-notification-content">
                    <span class="hf-notification-icon">${type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'}</span>
                    <span class="hf-notification-message">${this.escapeHTML(message)}</span>
                </div>
            `;

            document.body.appendChild(notification);

            setTimeout(() => {
                notification.classList.add('hf-notification-show');
            }, 10);

            setTimeout(() => {
                notification.classList.remove('hf-notification-show');
                setTimeout(() => notification.remove(), 300);
            }, 3000);
        }

        static formatDate(timestamp) {
            const date = new Date(timestamp);
            const now = new Date();
            const diff = now - date;

            if (diff < 3600000) { // < 1 hour
                return `${Math.floor(diff / 60000)} мин назад`;
            } else if (diff < 86400000) { // < 1 day
                return `${Math.floor(diff / 3600000)} ч назад`;
            } else {
                return date.toLocaleDateString('ru-RU');
            }
        }
    }

    // ======================
    // CONTENT FILTER
    // (JS-логика без изменений, она оптимальна)
    // ======================
    class ContentFilter {
        constructor(storage) {
            this.storage = storage;
            this.currentMenu = null;
        }

        init() {
            this.injectStyles();
            this.processInitialContent();
            this.addControlPanel();
            this.setupKeyboardShortcuts();
            this.observeDOM();
        }

        /**
         * ==========================================================
         * * СТИЛИЗАЦИЯ ИНТЕРФЕЙСА
         * (Полностью переработано под стиль Habr)
         * * ==========================================================
         */
        injectStyles() {
            GM_addStyle(`
                /* =================================
                   Общие переменные (Habr's Palette)
                   ================================= */
                :root {
                    --hf-font: Inter, sans-serif;
                    --hf-green: #31a34c; /* Habr accent green */
                    --hf-blue: #507299;  /* Habr user link blue */
                    --hf-text-primary: #111827; /* Habr dark text */
                    --hf-text-secondary: #6b7280; /* Habr gray text */
                    --hf-bg-white: #ffffff;
                    --hf-bg-gray-light: #f9fafb; /* Habr light bg */
                    --hf-bg-gray-hover: #f3f4f6;
                    --hf-border: #e5e7eb; /* Habr border */
                }
            
                /* =================================
                   Кнопка блокировки (⛔)
                   ================================= */
                .hf-block-btn {
                    display: inline-flex;
                    align-items: center;
                    justify-content: center;
                    width: 20px;
                    height: 20px;
                    margin-left: 6px;
                    background: none;
                    border: none;
                    cursor: pointer;
                    opacity: 0.35;
                    filter: grayscale(1);
                    transition: all 0.2s ease;
                    font-size: 14px;
                    vertical-align: middle;
                    border-radius: 4px;
                }

                .tm-publication-hub__link-container:hover .hf-block-btn,
                .tm-user-info__user:hover .hf-block-btn,
                .hf-block-btn:hover {
                    opacity: 0.95;
                    filter: none;
                    transform: scale(1.1);
                    background: var(--hf-bg-gray-hover);
                }

                .hf-block-btn.blocked {
                    opacity: 0.95 !important;
                    filter: none;
                }

                /* =================================
                   Контекстное меню
                   ================================= */
                .hf-context-menu {
                    position: fixed;
                    background: var(--hf-bg-white);
                    border-radius: 8px; /* Native border-radius */
                    border: 1px solid var(--hf-border);
                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* Native shadow */
                    min-width: 260px;
                    z-index: 10000;
                    animation: hf-menu-appear 0.15s ease-out;
                    overflow: hidden;
                    font-family: var(--hf-font);
                }

                @keyframes hf-menu-appear {
                    from {
                        opacity: 0;
                        transform: translateY(-5px) scale(0.98);
                    }
                    to {
                        opacity: 1;
                        transform: translateY(0) scale(1);
                    }
                }

                .hf-menu-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 12px 16px;
                    background: var(--hf-bg-gray-light); /* Native header bg */
                    border-bottom: 1px solid var(--hf-border);
                    color: var(--hf-text-primary);
                }

                .hf-menu-header strong {
                    font-size: 15px;
                    font-weight: 600;
                }

                .hf-menu-close {
                    background: none;
                    border: none;
                    color: var(--hf-text-secondary);
                    font-size: 24px;
                    cursor: pointer;
                    opacity: 0.7;
                    padding: 0;
                    width: 24px;
                    height: 24px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    transition: all 0.2s;
                }

                .hf-menu-close:hover {
                    opacity: 1;
                    color: var(--hf-text-primary);
                }

                .hf-menu-content {
                    padding: 8px;
                }

                .hf-menu-item {
                    display: flex;
                    align-items: center;
                    gap: 10px;
                    width: 100%;
                    padding: 10px 12px;
                    background: none;
                    border: none;
                    border-radius: 6px;
                    cursor: pointer;
                    font-size: 14px;
                    font-weight: 500;
                    color: var(--hf-text-primary);
                    text-align: left;
                    transition: background 0.2s;
                }

                .hf-menu-item:hover {
                    background: var(--hf-bg-gray-hover);
                }

                .hf-menu-icon {
                    font-size: 16px;
                }

                .hf-menu-divider {
                    height: 1px;
                    background: var(--hf-border);
                    margin: 8px 0;
                }

                .hf-menu-stats {
                    padding: 10px 12px;
                    background: var(--hf-bg-gray-light);
                    border-radius: 6px;
                    margin-top: 4px;
                }

                .hf-stat-item {
                    display: flex;
                    justify-content: space-between;
                    padding: 4px 0;
                    font-size: 13px;
                }

                .hf-stat-label {
                    color: var(--hf-text-secondary);
                }

                .hf-stat-value {
                    font-weight: 600;
                    color: var(--hf-text-primary);
                }

                /* =================================
                   Панель управления (Кнопка)
                   ================================= */
                #hf-control-panel {
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    z-index: 9999;
                    font-family: var(--hf-font);
                }

                .hf-panel-toggle {
                    width: 40px;
                    height: 40px;
                    border-radius: 8px;
                    background: var(--hf-bg-white);
                    border: 1px solid var(--hf-border);
                    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07);
                    cursor: pointer;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    transition: all 0.2s ease;
                    position: relative;
                }

                .hf-panel-toggle:hover {
                    transform: translateY(-1px);
                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
                    border-color: #d1d5db;
                }

                .hf-panel-icon {
                    font-size: 20px;
                }

                .hf-panel-badge {
                    position: absolute;
                    top: -6px;
                    right: -6px;
                    background: var(--hf-green); /* Habr Green */
                    color: var(--hf-bg-white);
                    font-size: 11px;
                    font-weight: 600;
                    padding: 1px 6px;
                    border-radius: 999px;
                    min-width: 18px;
                    text-align: center;
                    border: 2px solid var(--hf-bg-white);
                }

                /* =================================
                   Панель управления (Контент)
                   ================================= */
                .hf-panel-content {
                    position: absolute;
                    bottom: 54px;
                    right: 0;
                    width: 380px;
                    max-height: 600px;
                    background: var(--hf-bg-white);
                    border-radius: 12px;
                    border: 1px solid var(--hf-border);
                    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
                    opacity: 0;
                    visibility: hidden;
                    transform: translateY(10px) scale(0.98);
                    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
                    overflow: hidden;
                    display: flex;
                    flex-direction: column;
                }

                .hf-panel:not(.hf-panel-collapsed) .hf-panel-content {
                    opacity: 1;
                    visibility: visible;
                    transform: translateY(0) scale(1);
                }

                .hf-panel-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 16px 20px;
                    background: var(--hf-bg-gray-light);
                    border-bottom: 1px solid var(--hf-border);
                    color: var(--hf-text-primary);
                }

                .hf-panel-header h3 {
                    margin: 0;
                    font-size: 16px;
                    font-weight: 600;
                }

                .hf-panel-close {
                    background: none;
                    border: none;
                    color: var(--hf-text-secondary);
                    font-size: 28px;
                    cursor: pointer;
                    padding: 0;
                    width: 32px;
                    height: 32px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    opacity: 0.8;
                    transition: opacity 0.2s;
                }

                .hf-panel-close:hover {
                    opacity: 1;
                    color: var(--hf-text-primary);
                }

                .hf-panel-body {
                    padding: 16px;
                    overflow-y: auto;
                    max-height: 500px;
                }

                .hf-stats-summary {
                    margin-bottom: 20px;
                }

                .hf-stat-card {
                    text-align: center;
                    padding: 16px;
                    background: var(--hf-bg-gray-light);
                    border: 1px solid var(--hf-border);
                    border-radius: 8px;
                }

                .hf-stat-number {
                    font-size: 32px;
                    font-weight: 700;
                    color: var(--hf-green);
                    line-height: 1.2;
                }

                .hf-stat-label {
                    font-size: 14px;
                    color: var(--hf-text-secondary);
                    margin-top: 4px;
                }

                .hf-section {
                    margin-bottom: 20px;
                }

                .hf-section h4 {
                    font-size: 14px;
                    font-weight: 600;
                    margin: 0 0 10px 0;
                    color: var(--hf-text-primary);
                }

                .hf-blocked-list {
                    display: flex;
                    flex-direction: column;
                    gap: 6px;
                }

                .hf-blocked-item {
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    padding: 8px 10px;
                    background: var(--hf-bg-white);
                    border: 1px solid var(--hf-border);
                    border-radius: 8px;
                    transition: background 0.2s;
                }

                .hf-blocked-item:hover {
                    background: var(--hf-bg-gray-light);
                }

                .hf-blocked-item-inactive {
                    opacity: 0.7;
                }
                
                .hf-blocked-item-inactive:hover {
                    opacity: 1;
                }

                .hf-blocked-name {
                    flex: 1;
                    font-size: 13px;
                    color: var(--hf-text-primary);
                    font-weight: 500;
                    word-break: break-all;
                }

                .hf-blocked-count {
                    font-size: 12px;
                    font-weight: 600;
                    color: var(--hf-text-secondary);
                    margin-right: 8px;
                }

                .hf-unblock-btn,
                .hf-reactivate-btn {
                    border: 1px solid;
                    padding: 3px 8px;
                    border-radius: 6px;
                    cursor: pointer;
                    font-size: 13px;
                    font-weight: 600;
                    transition: all 0.2s;
                }
                
                /* Разблокировать (вторичное, "призрачное" действие) */
                .hf-unblock-btn {
                    background: transparent;
                    color: var(--hf-green);
                    border-color: var(--hf-green);
                }

                .hf-unblock-btn:hover {
                    background: #e7f4e8; /* Light green bg */
                }
                
                /* Снова блокировать (первичное действие) */
                .hf-reactivate-btn {
                    background: var(--hf-green);
                    color: var(--hf-bg-white);
                    border-color: var(--hf-green);
                }

                .hf-reactivate-btn:hover {
                    background: #2a8a40;
                    border-color: #2a8a40;
                }

                .hf-empty {
                    text-align: center;
                    padding: 16px;
                    color: var(--hf-text-secondary);
                    font-size: 13px;
                    border: 1px dashed var(--hf-border);
                    border-radius: 8px;
                }

                .hf-tip {
                    background: #fffbeb; /* Native Habr "info" yellow */
                    border: 1px solid #fde68a;
                    padding: 12px;
                    border-radius: 8px;
                    font-size: 13px;
                    color: #78350f;
                    margin-top: 16px;
                }

                /* =================================
                   Скрытие и Заглушки
                   ================================= */
                .hf-article-hidden {
                    opacity: 0 !important;
                    height: 0 !important;
                    overflow: hidden !important;
                    margin: 0 !important;
                    padding: 0 !important;
                    border: none !important;
                    transition: all 0.3s ease;
                }

                .hf-full-article-block {
                    margin: 16px 0;
                    padding: 16px 20px;
                    border-radius: 8px;
                    background: #fffbeb; /* Native info yellow */
                    border: 1px solid #fde68a;
                    color: #78350f;
                    font-size: 14px;
                    line-height: 1.5;
                    font-family: var(--hf-font);
                }

                .hf-full-article-title {
                    font-weight: 600;
                    color: #111827;
                    margin-bottom: 8px;
                }
                
                .hf-full-article-reasons {
                    margin: 8px 0 12px 0;
                    font-size: 13px;
                }
                
                .hf-full-article-reasons ul {
                    margin: 4px 0 0 18px;
                    padding: 0;
                }
                
                .hf-full-article-actions {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 8px;
                    margin-top: 12px;
                }
                
                .hf-full-article-btn {
                    padding: 6px 12px;
                    border-radius: 6px;
                    border: 1px solid;
                    cursor: pointer;
                    font-size: 13px;
                    font-weight: 600;
                    display: inline-flex;
                    align-items: center;
                    gap: 6px;
                    transition: all 0.2s;
                }
                
                /* Primary button (Green) */
                .hf-full-article-btn-primary {
                    background: var(--hf-green);
                    color: var(--hf-bg-white);
                    border-color: var(--hf-green);
                }
                .hf-full-article-btn-primary:hover {
                     background: #2a8a40;
                     border-color: #2a8a40;
                }
                
                /* Secondary button (Ghost) */
                .hf-full-article-btn-secondary {
                    background: var(--hf-bg-white);
                    color: var(--hf-text-primary);
                    border-color: #d1d5db; /* gray-300 */
                }
                .hf-full-article-btn-secondary:hover {
                    background: var(--hf-bg-gray-hover);
                    border-color: #9ca3af; /* gray-400 */
                }

                /* =================================
                   Уведомления
                   ================================= */
                .hf-notification {
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    background: var(--hf-bg-white);
                    padding: 16px 20px;
                    border-radius: 8px;
                    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
                    z-index: 10001;
                    transform: translateX(400px);
                    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                    border-left: 4px solid;
                    font-family: var(--hf-font);
                }

                .hf-notification-show {
                    transform: translateX(0);
                }

                .hf-notification-content {
                    display: flex;
                    align-items: center;
                    gap: 12px;
                }

                .hf-notification-icon {
                    font-size: 20px;
                }

                .hf-notification-message {
                    font-size: 14px;
                    font-weight: 500;
                    color: var(--hf-text-primary);
                }

                .hf-notification-success {
                    border-left-color: var(--hf-green);
                }

                .hf-notification-error {
                    border-left-color: #ef4444; /* red-500 */
                }

                .hf-notification-info {
                    border-left-color: #3b82f6; /* blue-500 */
                }
            `);
        }

        /**
         * ==========================================================
         * * ЛОГИКА СКРИПТА
         * (Оптимизированный JS без изменений)
         * * ==========================================================
         */

        processInitialContent() {
            document.querySelectorAll('.tm-article-snippet').forEach(article => {
                this.processArticleSnippet(article);
            });

            const presenter = document.querySelector('.tm-article-presenter');
            if (presenter) {
                this.processFullArticle(presenter);
            }
        }

        processArticleSnippet(article) {
            if (article.dataset.hfProcessed) return;
            article.dataset.hfProcessed = 'true';

            const evaluation = this.evaluateArticleContent(article);

            evaluation.tags.forEach(tag => {
                this.injectBlockButton(tag.container, 'tags', tag.name, tag.stats);
            });
            if (evaluation.author) {
                this.injectBlockButton(evaluation.author.container, 'authors', evaluation.author.name, evaluation.author.stats);
            }

            if (evaluation.shouldHide) {
                this.hideArticle(article, evaluation.hideReasons);
            }
        }

        processFullArticle(presenter) {
            if (presenter.dataset.hfProcessedFull) return;
            presenter.dataset.hfProcessedFull = 'true';

            const evaluation = this.evaluateArticleContent(presenter);

            evaluation.tags.forEach(tag => {
                this.injectBlockButton(tag.container, 'tags', tag.name, tag.stats);
            });
            if (evaluation.author) {
                this.injectBlockButton(evaluation.author.container, 'authors', evaluation.author.name, evaluation.author.stats);
            }

            if (evaluation.shouldHide) {
                this.hideFullArticle(presenter, evaluation.hideReasons);
            }
        }

        evaluateArticleContent(element) {
            let shouldHide = false;
            const hideReasons = [];
            const tags = [];
            let author = null;

            element.querySelectorAll('a.tm-publication-hub__link').forEach(tagLink => {
                const tagContainer = tagLink.closest('.tm-publication-hub__link-container') || tagLink.parentElement;
                const tagSpan = tagLink.querySelector('span');
                const tagName = (tagSpan ? tagSpan.textContent.trim() : tagLink.textContent.trim()).toLowerCase();
                if (!tagName || !tagContainer) return;

                const stats = this.storage.getStats('tags', tagName);
                tags.push({ name: tagName, stats: stats, container: tagContainer });

                if (this.storage.isBlocked('tags', tagName)) {
                    shouldHide = true;
                    if (!hideReasons.includes(`тег: ${tagName}`)) {
                        hideReasons.push(`тег: ${tagName}`);
                    }
                }
            });

            const authorLink = element.querySelector('a.tm-user-info__username');
            if (authorLink) {
                const authorContainer = authorLink.parentElement;
                if (authorContainer) {
                    const authorName = authorLink.textContent.trim();
                    const stats = this.storage.getStats('authors', authorName);
                    author = { name: authorName, stats: stats, container: authorContainer };

                    if (this.storage.isBlocked('authors', authorName)) {
                        shouldHide = true;
                        hideReasons.push(`автор: ${authorName}`);
                    }
                }
            }
            
            return { shouldHide, hideReasons, tags, author };
        }

        injectBlockButton(container, type, value, stats) {
            if (container.querySelector('.hf-block-btn')) return;

            const blockBtn = document.createElement('button');
            blockBtn.className = 'hf-block-btn';
            blockBtn.innerHTML = stats.blocked ? '🔴' : '⛔';
            blockBtn.title = stats.blocked ? 'Заблокировано' : 'Нажмите для блокировки';
            if (stats.blocked) blockBtn.classList.add('blocked');

            container.appendChild(blockBtn);

            blockBtn.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                this.showContextMenu(e, type, value);
            });
        }

        hideArticle(article, reasons) {
            if (!article.classList.contains('hf-article-hidden')) {
                reasons.forEach(reason => {
                    const [type, value] = reason.split(': ');
                    const typeKey = type === 'тег' ? 'tags' : 'authors';
                    this.storage.incrementHidden(typeKey, value);
                });
            }
            
            const container = article.closest('.tm-articles-list__item') || article;
            container.classList.add('hf-article-hidden');
        }

        hideFullArticle(articleRoot, reasons) {
            reasons.forEach(reason => {
                const [type, value] = reason.split(': ');
                const typeKey = type === 'тег' ? 'tags' : 'authors';
                this.storage.incrementHidden(typeKey, value);
            });

            const content = articleRoot.querySelector('.tm-article-presenter__body, .tm-article-presenter__content, article') || articleRoot;
            
            if (articleRoot.querySelector('.hf-full-article-block')) return;

            const placeholder = document.createElement('div');
            placeholder.className = 'hf-full-article-block';

            const reasonsList = reasons.map(r => `<li>${UIComponents.escapeHTML(r)}</li>`).join('');

            placeholder.innerHTML = `
                <div class="hf-full-article-title">Эта статья скрыта вашим фильтром контента</div>
                <div class="hf-full-article-reasons">
                    Причина${reasons.length > 1 ? 'ы' : ''}:
                    <ul>${reasonsList}</ul>
                </div>
                <div class="hf-full-article-actions">
                    <button class="hf-full-article-btn hf-full-article-btn-primary" data-action="show-once">
                        👁️ Показать только сейчас
                    </button>
                    <button class="hf-full-article-btn hf-full-article-btn-secondary" data-action="unblock">
                        ✅ Разблокировать критерии
                    </button>
                    <button class="hf-full-article-btn hf-full-article-btn-secondary" data-action="open-panel">
                        ⚙️ Открыть панель
                    </button>
                </div>
            `;

            content.style.display = 'none';
            content.parentNode.insertBefore(placeholder, content);

            placeholder.addEventListener('click', (e) => {
                const action = e.target.closest('[data-action]');
                if (!action) return;

                const actionName = action.dataset.action;

                if (actionName === 'show-once') {
                    content.style.display = '';
                    placeholder.remove();
                } else if (actionName === 'unblock') {
                    reasons.forEach(reason => {
                        const [type, value] = reason.split(': ');
                        const typeKey = type === 'тег' ? 'tags' : 'authors';
                        this.storage.removeBlock(typeKey, value);
                    });
                    UIComponents.createNotification('Блокировка для этой статьи снята', 'success');
                    content.style.display = '';
                    placeholder.remove();
                    this.refresh();
                } else if (actionName === 'open-panel') {
                    const panel = document.getElementById('hf-control-panel');
                    if (panel) {
                        panel.classList.remove('hf-panel-collapsed');
                    }
                }
            });
        }

        showContextMenu(event, type, value) {
            event.preventDefault();
            event.stopPropagation();

            if (this.currentMenu) {
                this.currentMenu.remove();
            }

            const stats = this.storage.getStats(type, value);
            const menu = UIComponents.createContextMenu(type, value, stats);

            const rect = event.target.getBoundingClientRect();
            menu.style.left = `${rect.left}px`;
            menu.style.top = `${rect.bottom + 8}px`; // Чуть ближе

            document.body.appendChild(menu);
            const menuRect = menu.getBoundingClientRect();
            if (menuRect.right > (window.innerWidth - 10)) {
                menu.style.left = `${window.innerWidth - menuRect.width - 10}px`;
            }
            if (menuRect.bottom > (window.innerHeight - 10)) {
                menu.style.top = `${rect.top - menuRect.height - 8}px`;
            }

            this.currentMenu = menu;

            menu.addEventListener('click', (e) => {
                const closeBtn = e.target.closest('.hf-menu-close');
                const blockBtn = e.target.closest('.hf-block-action');
                const unblockBtn = e.target.closest('.hf-unblock-action');

                if (closeBtn) {
                    menu.remove();
                    this.currentMenu = null;
                } else if (blockBtn) {
                    const duration = blockBtn.dataset.duration;
                    this.storage.addBlock(type, value, duration);
                    UIComponents.createNotification(`Заблокировано: ${value}`, 'success');
                    menu.remove();
                    this.currentMenu = null;
                    this.refresh();
                } else if (unblockBtn) {
                    this.storage.removeBlock(type, value);
                    UIComponents.createNotification(`Разблокировано: ${value}`, 'success');
                    menu.remove();
                    this.currentMenu = null;
                    this.refresh();
                }
            });

            setTimeout(() => {
                document.addEventListener('click', this.closeMenuHandler = (e) => {
                    if (!menu.contains(e.target)) {
                        menu.remove();
                        this.currentMenu = null;
                        document.removeEventListener('click', this.closeMenuHandler);
                    }
                });
            }, 100);
        }

        addControlPanel() {
            if (document.getElementById('hf-control-panel')) return;
            if (!document.body) {
                setTimeout(() => this.addControlPanel(), 100);
                return;
            }

            const panel = UIComponents.createControlPanel(this.storage);
            document.body.appendChild(panel);

            panel.addEventListener('click', (e) => {
                const toggleBtn = e.target.closest('.hf-panel-toggle');
                const closeBtn = e.target.closest('.hf-panel-close');
                const unblockBtn = e.target.closest('.hf-unblock-btn');
                const reactivateBtn = e.target.closest('.hf-reactivate-btn');

                if (toggleBtn) {
                    panel.classList.toggle('hf-panel-collapsed');
                } else if (closeBtn) {
                    panel.classList.add('hf-panel-collapsed');
                } else if (unblockBtn) {
                    const item = unblockBtn.closest('.hf-blocked-item');
                    const type = item.dataset.type;
                    const value = item.dataset.value;
                    this.storage.removeBlock(type, value);
                    UIComponents.createNotification(`Разблокировано: ${value}`, 'success');
                    this.refresh();
                } else if (reactivateBtn) {
                    const item = reactivateBtn.closest('.hf-blocked-item');
                    const type = item.dataset.type;
                    const value = item.dataset.value;
                    this.storage.addBlock(type, value);
                    UIComponents.createNotification(`Снова заблокировано: ${value}`, 'success');
                    this.refresh();
                }
            });
        }

        setupKeyboardShortcuts() {
            document.addEventListener('click', (e) => {
                if (!e.shiftKey) return;

                const tagLink = e.target.closest('a.tm-publication-hub__link');
                const authorLink = e.target.closest('a.tm-user-info__username');

                if (tagLink) {
                    e.preventDefault();
                    e.stopPropagation();
                    const tagSpan = tagLink.querySelector('span');
                    const tagName = (tagSpan ? tagSpan.textContent.trim() : tagLink.textContent.trim()).toLowerCase();
                    this.storage.addBlock('tags', tagName);
                    UIComponents.createNotification(`⚡ Быстро заблокирован тег: ${tagName}`, 'success');
                    this.refresh();
                } else if (authorLink) {
                    e.preventDefault();
                    e.stopPropagation();
                    const authorName = authorLink.textContent.trim();
                    this.storage.addBlock('authors', authorName);
                    UIComponents.createNotification(`⚡ Быстро заблокирован автор: ${authorName}`, 'success');
                    this.refresh();
                }
            });
        }

        observeDOM() {
            const observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                    if (mutation.type !== 'childList') continue;

                    for (const node of mutation.addedNodes) {
                        if (node.nodeType !== Node.ELEMENT_NODE) continue;

                        if (node.matches('.tm-article-snippet')) {
                            this.processArticleSnippet(node);
                        }
                        else if (node.matches('.tm-article-presenter')) {
                            this.processFullArticle(node);
                        }
                        else {
                            node.querySelectorAll('.tm-article-snippet').forEach(snippet => this.processArticleSnippet(snippet));
                            const presenter = node.querySelector('.tm-article-presenter');
                            if (presenter) this.processFullArticle(presenter);
                        }
                    }
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        refresh() {
            let panelWasOpen = false;
            const oldPanel = document.getElementById('hf-control-panel');
            if (oldPanel) {
                panelWasOpen = !oldPanel.classList.contains('hf-panel-collapsed');
                oldPanel.remove();
            }

            this.storage.data = this.storage.load();

            this.addControlPanel();
            const newPanel = document.getElementById('hf-control-panel');
            if (newPanel && panelWasOpen) {
                newPanel.classList.remove('hf-panel-collapsed');
            }

            this.reEvaluateProcessedContent();
        }

        reEvaluateProcessedContent() {
            document.querySelectorAll('.tm-article-snippet[data-hf-processed]').forEach(article => {
                const evaluation = this.evaluateArticleContent(article);
                
                evaluation.tags.forEach(tag => {
                    const btn = tag.container.querySelector('.hf-block-btn');
                    if (btn) {
                        btn.innerHTML = tag.stats.blocked ? '🔴' : '⛔';
                        btn.title = tag.stats.blocked ? 'Заблокировано' : 'Нажмите для блокировки';
                        btn.classList.toggle('blocked', tag.stats.blocked);
                    }
                });
                if (evaluation.author) {
                    const btn = evaluation.author.container.querySelector('.hf-block-btn');
                     if (btn) {
                        btn.innerHTML = evaluation.author.stats.blocked ? '🔴' : '⛔';
                        btn.title = evaluation.author.stats.blocked ? 'Заблокировано' : 'Нажмите для блокировки';
                        btn.classList.toggle('blocked', evaluation.author.stats.blocked);
                    }
                }

                const container = article.closest('.tm-articles-list__item') || article;
                container.classList.toggle('hf-article-hidden', evaluation.shouldHide);
            });

            const presenter = document.querySelector('.tm-article-presenter[data-hf-processed-full]');
            if (presenter) {
                const evaluation = this.evaluateArticleContent(presenter);
                const content = presenter.querySelector('.tm-article-presenter__body, .tm-article-presenter__content, article');
                const placeholder = presenter.querySelector('.hf-full-article-block');
                
                evaluation.tags.forEach(tag => {
                    const btn = tag.container.querySelector('.hf-block-btn');
                    if(btn) {
                        btn.innerHTML = tag.stats.blocked ? '🔴' : '⛔';
                        btn.classList.toggle('blocked', tag.stats.blocked);
                    }
                });
                if (evaluation.author) {
                     const btn = evaluation.author.container.querySelector('.hf-block-btn');
                     if(btn) {
                        btn.innerHTML = evaluation.author.stats.blocked ? '🔴' : '⛔';
                        btn.classList.toggle('blocked', evaluation.author.stats.blocked);
                     }
                }

                if (evaluation.shouldHide && !placeholder) {
                    this.hideFullArticle(presenter, evaluation.hideReasons);
                } else if (!evaluation.shouldHide && placeholder) {
                    if(content) content.style.display = '';
                    placeholder.remove();
                }
            }
        }
    }

    // ======================
    // INIT
    // ======================
    console.log('🎯 Habr Content Filter Pro v2.5 (Native UI) загружается...');

    function initialize() {
        console.log('📦 Инициализация фильтра...');
        try {
            const storage = new StorageManager();
            const filter = new ContentFilter(storage);
            filter.init();
            console.log('✅ Фильтр успешно запущен!');
        } catch (error) {
            console.error('❌ Ошибка при инициализации фильтра:', error);
            console.error('Stack trace:', error.stack);
        }
    }

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

})();