ITD Feed Filter

Фильтрация ленты в ИТД

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         ITD Feed Filter
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Фильтрация ленты в ИТД
// @author       0wn3df1x
// @license      MIT
// @icon         https://xn--d1ah4a.com/favicon.ico
// @match        *://*.xn--d1ah4a.com/*
// @match        *://*.итд.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const customStyles = document.createElement('style');
    customStyles.innerHTML = `
        #itd-filter-list::-webkit-scrollbar { width: 6px; }
        #itd-filter-list::-webkit-scrollbar-track { background: transparent; }
        #itd-filter-list::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, 0.3); border-radius: 3px; }
        #itd-filter-list::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
    `;
    document.head.appendChild(customStyles);

    // Функция для извлечения всего полезного текста из объекта поста
    function extractTextPayload(p, elements) {
        if (!p) return;
        if (p.content) elements.push(p.content);
        if (p.author) {
            if (p.author.username) elements.push(p.author.username);
            if (p.author.displayName) elements.push(p.author.displayName);
        }
        if (p.dominantEmoji) elements.push(p.dominantEmoji);

        if (p.spans && Array.isArray(p.spans)) {
            p.spans.forEach(span => {
                if (span.tag) {
                    elements.push(span.tag);
                    if (span.type === 'hashtag') elements.push('#' + span.tag);
                }
            });
        }
    }

    function checkSpam(post, words) {
        if (!post) return false;

        let elements = [];

        // Проверяем сам пост
        extractTextPayload(post, elements);

        // Если это репост, обязательно проверяем и оригинальный пост
        if (post.originalPost) {
            extractTextPayload(post.originalPost, elements);
        }

        const textPayload = elements.join(' ').toLowerCase();
        return words.some(w => textPayload.includes(w));
    }

    function getActiveWords() {
        return GM_getValue('itd_filter_words', []).map(w => w.toLowerCase());
    }

    function isProfilePage() {
        return window.location.pathname.startsWith('/@');
    }

    const _window = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

    // --- 1. ПЕРЕХВАТ FETCH ---
    const origFetch = _window.fetch;
    _window.fetch = async function(...args) {
        const urlStr = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url ? args[0].url : '');

        if (urlStr.includes('/api/posts') && (!args[1] || args[1].method === 'GET' || !args[1].method)) {
            const filterActive = GM_getValue('itd_filter_active', true);
            const words = getActiveWords();

            if (!filterActive || words.length === 0 || isProfilePage()) {
                return origFetch.apply(this, args);
            }

            try {
                let response = await origFetch.apply(this, args);
                if (!response.ok) return response;

                let json = await response.clone().json();

                if (json && json.data && json.data.posts) {
                    json.data.posts = json.data.posts.filter(p => !checkSpam(p, words));

                    let maxRecursion = 5;
                    while (json.data.posts.length < 5 && json.data.pagination && json.data.pagination.hasMore && maxRecursion > 0) {
                        maxRecursion--;

                        const nextCursor = json.data.pagination.nextCursor;
                        if (!nextCursor) break;

                        const nextUrl = new URL(urlStr, _window.location.origin);
                        nextUrl.searchParams.set('cursor', nextCursor);

                        const nextArgs = [...args];
                        nextArgs[0] = nextUrl.toString();

                        const nextRes = await origFetch.apply(this, nextArgs);
                        if (!nextRes.ok) break;

                        const nextJson = await nextRes.json();
                        if (nextJson && nextJson.data && nextJson.data.posts) {
                            const cleanNextPosts = nextJson.data.posts.filter(p => !checkSpam(p, words));
                            json.data.posts = json.data.posts.concat(cleanNextPosts);
                            json.data.pagination = nextJson.data.pagination;
                        } else {
                            break;
                        }
                    }

                    const newHeaders = new Headers(response.headers);
                    newHeaders.delete('content-length');

                    return new Response(JSON.stringify(json), {
                        status: response.status,
                        statusText: response.statusText,
                        headers: newHeaders
                    });
                }
            } catch (e) {
                console.error("[ITD Filter] Error in Ghost Fetch:", e);
            }
        }
        return origFetch.apply(this, args);
    };

    // --- 2. ПЕРЕХВАТ JSON.PARSE ---
    const origParse = _window.JSON.parse;
    _window.JSON.parse = function(text, reviver) {
        const data = origParse(text, reviver);
        try {
            const filterActive = GM_getValue('itd_filter_active', true);
            const words = getActiveWords();
            if (filterActive && words.length > 0 && data && data.data && Array.isArray(data.data.posts) && !isProfilePage()) {
                data.data.posts = data.data.posts.filter(p => !checkSpam(p, words));
            }
        } catch(e) {}
        return data;
    };

    // --- 3. ВСТРАИВАНИЕ КНОПКИ-ШЕСТЕРЕНКИ ---
    function injectTools() {
        const panel = document.querySelector('.yhENW-7a');
        if (!panel) return;

        const tabs = Array.from(panel.querySelectorAll('button.DPhGAlZS'));
        const forYouBtn = tabs.find(btn => btn.textContent.trim().includes('Для вас'));

        if (forYouBtn && !forYouBtn.querySelector('.itd-filter-btn')) {
            const toolsContainer = document.createElement('span');
            toolsContainer.className = 'itd-filter-btn';
            toolsContainer.style.cssText = `
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 28px;
                height: 28px;
                margin-right: 8px;
                z-index: 10;
                transform: translateY(3px);
                background: var(--bg-tertiary);
                color: var(--text-secondary);
                border-radius: 50%;
                cursor: pointer;
                transition: background 0.2s ease, color 0.2s ease;
            `;

            toolsContainer.innerHTML = `
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <circle cx="12" cy="12" r="3"></circle>
                    <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
                </svg>
            `;

            toolsContainer.onmouseover = () => {
                toolsContainer.style.background = 'var(--bg-hover)';
                toolsContainer.style.color = 'var(--text-primary)';
            };
            toolsContainer.onmouseout = () => {
                toolsContainer.style.background = 'var(--bg-tertiary)';
                toolsContainer.style.color = 'var(--text-secondary)';
            };

            forYouBtn.insertBefore(toolsContainer, forYouBtn.firstChild);

            toolsContainer.addEventListener('click', (e) => {
                e.stopPropagation();
                e.preventDefault();
                openModal();
            });
        }
    }

    // --- 4. МОДАЛЬНОЕ ОКНО ---
    function openModal() {
        if (document.getElementById('itd-native-filter-modal')) return;

        let initialWords = JSON.stringify(GM_getValue('itd_filter_words', []));
        let initialActive = GM_getValue('itd_filter_active', true);

        const overlay = document.createElement('div');
        overlay.id = 'itd-native-filter-modal';
        overlay.style.cssText = `
            position: fixed; inset: 0; z-index: 10000;
            display: flex; align-items: center; justify-content: center;
            background: rgba(0, 0, 0, 0.6);
            backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
        `;

        const modal = document.createElement('div');
        modal.style.cssText = `
            background: var(--block-bg);
            color: var(--text-primary);
            padding: 24px;
            border-radius: 24px;
            width: 420px;
            max-width: 90vw;
            box-shadow: var(--shadow-elevated);
            display: flex; flex-direction: column; gap: 20px;
            max-height: 85vh;
            border: 1px solid var(--border-color);
        `;

        modal.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center;">
                <h2 style="font-size: 20px; font-weight: 600; margin: 0; color: var(--text-primary);">Настройки фильтра</h2>

                <label style="display: flex; align-items: center; gap: 12px; cursor: pointer; font-size: 14px; color: var(--text-secondary); font-weight: 500;">
                    Вкл
                    <div id="itd-toggle-ui" style="position: relative; width: 44px; height: 24px; background: ${initialActive ? 'var(--toggle-active-bg)' : 'var(--bg-tertiary)'}; border-radius: 12px; transition: background 0.2s;">
                        <div id="itd-toggle-knob" style="position: absolute; top: 2px; left: ${initialActive ? '22px' : '2px'}; width: 20px; height: 20px; background: var(--bg-primary); border-radius: 50%; transition: left 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.2);"></div>
                    </div>
                    <input type="checkbox" id="itd-filter-toggle" ${initialActive ? 'checked' : ''} style="display: none;">
                </label>
            </div>

            <div style="display: flex; gap: 8px;">
                <input type="text" id="itd-filter-input" placeholder="Хештег или слово..."
                    style="flex-grow: 1; padding: 12px 16px; border-radius: 12px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); outline: none; font-size: 14px; transition: border 0.2s;">
                <button id="itd-filter-add"
                    style="padding: 0 20px; background: var(--accent-primary); color: white; border: none; border-radius: 12px; cursor: pointer; font-weight: 600; font-size: 18px; transition: opacity 0.2s;">+</button>
            </div>

            <div id="itd-filter-list" style="overflow-y: auto; border: 1px solid var(--border-color); border-radius: 12px; padding: 8px; background: var(--bg-secondary); flex-grow: 1; min-height: 150px; max-height: 250px; display: flex; flex-direction: column; gap: 4px;"></div>

            <div style="display: flex; gap: 8px;">
                <button id="itd-filter-export" style="flex: 1; padding: 10px; background: var(--bg-tertiary); color: var(--text-primary); border: none; border-radius: 12px; cursor: pointer; font-weight: 500; font-size: 13px; transition: background 0.2s;">Экспорт</button>
                <button id="itd-filter-import" style="flex: 1; padding: 10px; background: var(--bg-tertiary); color: var(--text-primary); border: none; border-radius: 12px; cursor: pointer; font-weight: 500; font-size: 13px; transition: background 0.2s;">Импорт</button>
                <button id="itd-filter-clear"  style="flex: 1; padding: 10px; background: rgba(239, 68, 68, 0.1); color: #ef4444; border: none; border-radius: 12px; cursor: pointer; font-weight: 500; font-size: 13px; transition: background 0.2s;">Очистить</button>
            </div>

            <textarea id="itd-filter-io" placeholder="Вставьте список через точку с запятой (;)"
                style="width: 100%; height: 80px; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 12px; padding: 12px; display: none; resize: vertical; font-size: 13px; outline: none;"></textarea>

            <button id="itd-filter-close"
                style="width: 100%; padding: 12px; background: var(--text-primary); color: var(--bg-primary); border: none; border-radius: 16px; cursor: pointer; font-weight: 600; font-size: 15px; margin-top: 8px; transition: opacity 0.2s;">Готово</button>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        const addHover = (id) => {
            const el = document.getElementById(id);
            if(el) {
                el.onmouseover = () => el.style.opacity = '0.8';
                el.onmouseout = () => el.style.opacity = '1';
            }
        };
        addHover('itd-filter-add'); addHover('itd-filter-export'); addHover('itd-filter-import'); addHover('itd-filter-clear'); addHover('itd-filter-close');

        const inputEl = document.getElementById('itd-filter-input');
        inputEl.onfocus = () => inputEl.style.borderColor = 'var(--accent-primary)';
        inputEl.onblur = () => inputEl.style.borderColor = 'var(--border-color)';

        let filterWords = GM_getValue('itd_filter_words', []);
        const listContainer = document.getElementById('itd-filter-list');
        const ioArea = document.getElementById('itd-filter-io');
        const toggleCheckbox = document.getElementById('itd-filter-toggle');
        const toggleUi = document.getElementById('itd-toggle-ui');
        const toggleKnob = document.getElementById('itd-toggle-knob');

        function saveState() {
            GM_setValue('itd_filter_words', filterWords);
            GM_setValue('itd_filter_active', toggleCheckbox.checked);
        }

        function renderList() {
            listContainer.innerHTML = '';
            if (filterWords.length === 0) {
                listContainer.innerHTML = '<div style="color: var(--text-tertiary); text-align: center; padding: 30px 10px; font-size: 13px;">Список пуст</div>';
                return;
            }
            filterWords.forEach((word, index) => {
                const item = document.createElement('div');
                item.style.cssText = `
                    display: flex; justify-content: space-between; align-items: center;
                    padding: 8px 12px; background: var(--block-bg);
                    border: 1px solid var(--border-color); border-radius: 8px;
                `;

                const textSpan = document.createElement('span');
                textSpan.textContent = word;
                textSpan.style.cssText = 'word-break: break-word; font-size: 14px; font-weight: 500; color: var(--text-primary);';

                const removeBtn = document.createElement('button');
                removeBtn.innerHTML = `
                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"></path></svg>
                `;
                removeBtn.style.cssText = `
                    background: transparent; color: var(--text-tertiary); border: none;
                    border-radius: 50%; width: 24px; height: 24px; cursor: pointer;
                    display: flex; justify-content: center; align-items: center; transition: color 0.2s;
                `;
                removeBtn.onmouseover = () => removeBtn.style.color = '#ef4444';
                removeBtn.onmouseout = () => removeBtn.style.color = 'var(--text-tertiary)';

                removeBtn.onclick = () => {
                    filterWords.splice(index, 1);
                    saveState();
                    renderList();
                };

                item.appendChild(textSpan);
                item.appendChild(removeBtn);
                listContainer.appendChild(item);
            });
        }

        toggleCheckbox.onchange = (e) => {
            const isChecked = e.target.checked;
            toggleUi.style.background = isChecked ? 'var(--toggle-active-bg)' : 'var(--bg-tertiary)';
            toggleKnob.style.left = isChecked ? '22px' : '2px';
            saveState();
        };

        document.getElementById('itd-filter-add').onclick = () => {
            const val = inputEl.value.trim().toLowerCase();
            if (val && !filterWords.includes(val)) {
                filterWords.push(val);
                inputEl.value = '';
                saveState();
                renderList();
            }
        };

        inputEl.onkeypress = (e) => { if (e.key === 'Enter') document.getElementById('itd-filter-add').click(); };

        document.getElementById('itd-filter-clear').onclick = () => {
            if (confirm('Очистить фильтр?')) {
                filterWords = [];
                saveState();
                renderList();
            }
        };

        document.getElementById('itd-filter-export').onclick = () => {
            ioArea.style.display = 'block';
            ioArea.value = filterWords.join(';');
            ioArea.select();
        };

        document.getElementById('itd-filter-import').onclick = () => {
            if (ioArea.style.display === 'none' || ioArea.value.trim() === '') {
                ioArea.style.display = 'block';
                ioArea.value = '';
            } else {
                const newWords = ioArea.value.split(';').map(w => w.trim().toLowerCase()).filter(w => w !== '');
                newWords.forEach(w => {
                    if (!filterWords.includes(w)) filterWords.push(w);
                });
                ioArea.value = '';
                ioArea.style.display = 'none';
                saveState();
                renderList();
            }
        };

        function closeModal() {
            saveState();
            const currentWords = JSON.stringify(filterWords);
            const currentActive = toggleCheckbox.checked;

            if (currentWords !== initialWords || currentActive !== initialActive) {
                location.reload();
            } else {
                overlay.remove();
            }
        }

        document.getElementById('itd-filter-close').onclick = closeModal;
        overlay.onclick = (e) => { if (e.target === overlay) closeModal(); };

        renderList();
    }

    const observer = new MutationObserver(() => {
        if (document.body) {
            injectTools();
        }
    });
    observer.observe(document.documentElement, { childList: true, subtree: true });

})();