Ekşi Author Filter

Filters entries from a remote list (hide/collapse) and adds profile page warnings for Ekşi Sözlük.

当前为 2025-04-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Ekşi Author Filter
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Filters entries from a remote list (hide/collapse) and adds profile page warnings for Ekşi Sözlük.
// @author       @protono
// @match        *://eksisozluk.com/*
// @match        *://eksisozluk.com/
// @match        *://eksisozluk.com/*--*
// @match        *://eksisozluk.com/basliklar/gundem*
// @match        *://eksisozluk.com/basliklar/bugun*
// @match        *://eksisozluk.com/basliklar/populer*
// @match        *://eksisozluk.com/basliklar/debe*
// @match        *://eksisozluk.com/basliklar/kanal/*
// @match        *://eksisozluk.com/biri/*
// @exclude      *://eksisozluk.com/mesaj/*
// @exclude      *://eksisozluk.com/ayarlar/*
// @exclude      *://eksisozluk.com/hesap/*
// @exclude      *://eksisozluk.com/tercihler/*
// @icon         https://eksisozluk.com/favicon.ico
// @connect      raw.githubusercontent.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_deleteValue
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(async () => {
    'use strict';

    const SCRIPT_NAME = "Ekşi Sözlük Unified Filter";
    const PRIMARY_LIST_URL = "https://raw.githubusercontent.com/bat9254/troll-list/refs/heads/main/list.txt";
    const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
    const NETWORK_TIMEOUT_MS = 20000;
    const LOG_PREFIX = `[${SCRIPT_NAME}]`;
    const DEBOUNCE_DELAY_MS = 300;
    const SAVE_COUNT_DEBOUNCE_MS = 2500;
    const TOPIC_WARNING_THRESHOLD = 3;
    const CSS_PREFIX = "eusf-";

    const KEY_PAUSED = "eusf_paused_v1";
    const KEY_MODE = "eusf_filterMode_v1";
    const KEY_SHOW_WARNING = "eusf_showTopicWarning_v1";
    const KEY_LIST_RAW = "eusf_authorListRaw_v1";
    const KEY_LAST_UPDATE = "eusf_lastUpdateTime_v1";
    const KEY_TOTAL_FILTERED = "eusf_totalFiltered_v1";

    GM_addStyle(`
        .${CSS_PREFIX}topic-warning { background-color:#fff0f0; border:1px solid #d9534f; border-left:3px solid #d9534f; border-radius:3px; padding:2px 6px; margin-left:8px; font-size:0.85em; color:#a94442; display:inline-block; vertical-align:middle; cursor:help; font-weight:bold; }
        .${CSS_PREFIX}hidden { display: none !important; }
        .${CSS_PREFIX}collapsed > .content,
        .${CSS_PREFIX}collapsed > footer > .feedback-container,
        .${CSS_PREFIX}collapsed > footer .entry-footer-bottom > .footer-info > div:not(#entry-nick-container):not(:has(.entry-date)) { display: none !important; }
        .${CSS_PREFIX}collapsed > footer, .${CSS_PREFIX}collapsed footer > .info, .${CSS_PREFIX}collapsed footer .entry-footer-bottom { min-height: 1px; }
        .${CSS_PREFIX}collapsed #entry-nick-container, .${CSS_PREFIX}collapsed .entry-date { display:inline-block !important; visibility:visible !important; opacity:1 !important; }
        .${CSS_PREFIX}collapsed { min-height:35px !important; padding-bottom:0 !important; margin-bottom:10px !important; border-left:3px solid #ffcccc !important; background-color:rgba(128,128,128,0.03); overflow:hidden; }
        .${CSS_PREFIX}collapse-placeholder { min-height:25px; background-color:transparent; border:none; padding:6px 10px 6px 12px; margin-bottom:0px; font-style:normal; color:#6c757d; position:relative; display:flex; align-items:center; flex-wrap:wrap; box-sizing:border-box; }
        .${CSS_PREFIX}collapse-placeholder .${CSS_PREFIX}collapse-icon { margin-right:6px; opacity:0.9; font-style:normal; display:inline-block; color:#dc3545; cursor:help; }
        .${CSS_PREFIX}collapse-placeholder .${CSS_PREFIX}collapse-text { margin-right:10px; flex-grow:1; display:inline-block; font-size:0.9em; font-weight:500; }
        .${CSS_PREFIX}collapse-placeholder .${CSS_PREFIX}collapse-text strong { color:#dc3545; font-weight:600; }
        .${CSS_PREFIX}collapse-placeholder .${CSS_PREFIX}show-link { font-style:normal; flex-shrink:0; margin-left:auto; }
        .${CSS_PREFIX}collapse-placeholder .${CSS_PREFIX}show-link a { cursor:pointer; text-decoration:none; color:#0d6efd; font-size:0.9em; padding:1px 4px; border-radius:3px; font-weight:bold; border:1px solid transparent; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; }
        .${CSS_PREFIX}collapse-placeholder .${CSS_PREFIX}show-link a::before { content:"» "; opacity:0.7; }
        .${CSS_PREFIX}collapse-placeholder .${CSS_PREFIX}show-link a:hover { color:#0a58ca; text-decoration:underline; background-color:rgba(13,110,253,0.1); border-color:rgba(13,110,253,0.2); }
        .${CSS_PREFIX}opened-warning { font-size:0.8em; color:#856404; background-color:#fff3cd; border:1px solid #ffeeba; border-radius:3px; padding:1px 4px; margin-left:8px; vertical-align:middle; cursor:help; display:inline-block; font-style:normal; font-weight:bold; }
        .${CSS_PREFIX}profile-warning { margin-left: 10px; padding: 2px 6px; font-size: 0.85em; font-weight: bold; color: #a94442; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; vertical-align: middle; cursor: help; }
    `);

    const logger = {
        log: (...args) => console.log(LOG_PREFIX, ...args),
        warn: (...args) => console.warn(LOG_PREFIX, ...args),
        error: (...args) => console.error(LOG_PREFIX, ...args),
        debug: (...args) => console.debug(LOG_PREFIX, ...args),
    };

    const debounce = (func, wait) => {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    };

    const requiredGmFunctions = ['GM_getValue', 'GM_setValue', 'GM_xmlhttpRequest', 'GM_registerMenuCommand', 'GM_addStyle', 'GM_deleteValue'];
    if (requiredGmFunctions.some(fn => typeof window[fn] !== 'function')) {
        const missing = requiredGmFunctions.filter(fn => typeof window[fn] !== 'function');
        const errorMsg = `Hata: Gerekli Tampermonkey API fonksiyonları eksik: ${missing.join(', ')}! Script çalışmayacak. Lütfen Tampermonkey'in güncel olduğundan ve script'e @grant yetkilerinin verildiğinden emin olun.`;
        logger.error(errorMsg);
        if (typeof window.alert === 'function') {
            alert(`${SCRIPT_NAME} - Hata:\n${errorMsg}`);
        }
        return;
    }

    function showFeedback(title, text, options = {}) {
        const { isError = false, silent = false } = options;
        const prefix = isError ? "Hata" : "Bilgi";
        (isError ? logger.error : logger.log)(`${prefix}: ${title}`, text);
        if (!silent && typeof window.alert === 'function') {
            alert(`[${SCRIPT_NAME}] ${prefix}: ${title}\n\n${text}`);
        }
    }

    let config = {};
    let filteredAuthorsSet = new Set();
    let filteredListSize = 0;
    let filteredEntryCountOnPage = 0;
    let firstEntryAuthorFilteredOnPage = false;
    let topicWarningElement = null;
    let isFirstEntryOnPageProcessed = false;
    let entryListContainerEl = null;

    async function loadConfig() {
        logger.debug("Yapılandırma yükleniyor...");
        try {
            const results = await Promise.allSettled([
                GM_getValue(KEY_PAUSED, false),
                GM_getValue(KEY_MODE, "collapse"),
                GM_getValue(KEY_SHOW_WARNING, true),
                GM_getValue(KEY_LIST_RAW, ""),
                GM_getValue(KEY_LAST_UPDATE, 0),
                GM_getValue(KEY_TOTAL_FILTERED, 0)
            ]);

            const getValueFromResult = (result, defaultValue, keyName) => {
                if (result.status === 'fulfilled') {
                    return result.value;
                } else {
                    logger.warn(`'${keyName}' yüklenemedi, varsayılan (${defaultValue}) kullanılıyor. Hata:`, result.reason);
                    return defaultValue;
                }
            };

            config = {
                paused: getValueFromResult(results[0], false, KEY_PAUSED),
                filterMode: getValueFromResult(results[1], "collapse", KEY_MODE),
                showWarning: getValueFromResult(results[2], true, KEY_SHOW_WARNING),
                listRaw: getValueFromResult(results[3], "", KEY_LIST_RAW),
                lastUpdate: getValueFromResult(results[4], 0, KEY_LAST_UPDATE),
                totalFiltered: getValueFromResult(results[5], 0, KEY_TOTAL_FILTERED)
            };

            filteredAuthorsSet = parseAuthorList(config.listRaw);
            filteredListSize = filteredAuthorsSet.size;
            logger.log(`Yapılandırma: Durum: ${config.paused ? 'Duraklatıldı' : 'Aktif'}, Mod: ${config.filterMode}, Uyarılar: ${config.showWarning ? 'Açık' : 'Kapalı'}, Liste Boyutu: ${filteredListSize}, Toplam Filtrelenen: ${config.totalFiltered}`);

        } catch (err) {
            logger.error("Yapılandırma yüklenemedi:", err);
            config = { paused: false, filterMode: 'collapse', showWarning: true, listRaw: '', lastUpdate: 0, totalFiltered: 0 };
            filteredAuthorsSet = new Set();
            filteredListSize = 0;
            showFeedback("Yapılandırma Hatası", "Ayarlar yüklenemedi! Varsayılanlar kullanılıyor.", { isError: true });
        }
    }

    const debouncedSaveTotalFiltered = debounce(async (count) => {
        try {
            await GM_setValue(KEY_TOTAL_FILTERED, count);
            logger.debug(`Toplam filtreleme kaydedildi: ${count}`);
        } catch (err) {
            logger.warn("Toplam filtreleme kaydedilemedi:", err);
        }
    }, SAVE_COUNT_DEBOUNCE_MS);


    const fetchList = () => new Promise((resolve, reject) => {
        logger.debug(`Liste isteniyor: ${PRIMARY_LIST_URL}`);
        GM_xmlhttpRequest({
            method: "GET",
            url: PRIMARY_LIST_URL,
            timeout: NETWORK_TIMEOUT_MS,
            responseType: 'text',
            headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' },
            onload: response => {
                if (response.status >= 200 && response.status < 300) {
                    logger.debug(`Liste alındı (HTTP ${response.status}). Boyut: ${response.responseText?.length ?? 0} bytes.`);
                    resolve(response.responseText ?? "");
                } else {
                    const errorMsg = `Liste alınamadı. Yanıt: HTTP ${response.status} ${response.statusText || ''}`;
                    logger.warn(errorMsg);
                    reject(new Error(errorMsg));
                }
            },
            onerror: response => {
                const errorMsg = `Liste çekme hatası: ${response.statusText || 'Ağ hatası'}`;
                logger.error(errorMsg, response);
                reject(new Error(errorMsg));
            },
            ontimeout: () => {
                const errorMsg = `Liste çekme zaman aşımı (${NETWORK_TIMEOUT_MS / 1000}s).`;
                logger.error(errorMsg);
                reject(new Error(errorMsg));
            }
        });
    });

    const parseAuthorList = (rawText) => {
        if (!rawText || typeof rawText !== 'string') {
            if (rawText) logger.warn("Liste metni geçersiz.");
            return new Set();
        }
        try {
            const authors = rawText.split(/\r?\n/)
                .map(line => line.replace(/#.*$/, '').trim().toLowerCase())
                .filter(line => line.length > 0);
            logger.debug(`Liste ayrıştırıldı, ${authors.length} yazar bulundu.`);
            return new Set(authors);
        } catch (err) {
            logger.error("Liste ayrıştırılamadı:", err);
            showFeedback("Liste Hatası", `Liste işlenemedi. Hata: ${err.message}`, { isError: true });
            return new Set();
        }
    };

    const syncList = async (force = false) => {
        logger.log(`Liste güncellemesi ${force ? 'zorlanıyor' : 'kontrol ediliyor'}...`);
        let newRawText;
        try {
            newRawText = await fetchList();
        } catch (err) {
            logger.error("Liste çekme hatası:", err.message);
            if (force || filteredListSize === 0) {
                showFeedback("Güncelleme Başarısız", `Liste alınamadı.\nHata: ${err.message}\nMevcut liste kullanılacak.`, { isError: true });
            }
            return false;
        }

        if (!force && config.listRaw === newRawText) {
            logger.log("Liste değişmemiş.");
            config.lastUpdate = Date.now();
            GM_setValue(KEY_LAST_UPDATE, config.lastUpdate).catch(e => logger.warn("Zaman damgası kaydı başarısız:", e));
            return false;
        }

        logger.log(force ? "Zorunlu güncelleme/liste değişmiş, işleniyor." : "Liste değişmiş, güncelleniyor.");
        let newListSet;
        try {
            newListSet = parseAuthorList(newRawText);
        } catch (err) {
            logger.error("Liste işleme hatası (syncList):", err);
            return false;
        }

        if (filteredListSize > 0 && newListSet.size === 0 && newRawText.trim().length > 0) {
            logger.warn("Yeni liste alındı ancak ayrıştırma boş! Eski liste korunuyor.");
            showFeedback("Güncelleme Uyarısı", "Yeni liste boş sonuç verdi (format hatası?). Eski liste kullanılıyor.", { isError: true });
            config.lastUpdate = Date.now();
            GM_setValue(KEY_LAST_UPDATE, config.lastUpdate).catch(e=>logger.warn("Zaman damgası kaydı (ayrıştırma hatası) başarısız:", e));
            return false;
        }

        const oldSize = filteredListSize;
        filteredAuthorsSet = newListSet;
        filteredListSize = filteredAuthorsSet.size;
        config.listRaw = newRawText;
        config.lastUpdate = Date.now();
        logger.log(`Liste güncellendi. Eski: ${oldSize}, Yeni: ${filteredListSize}`);

        try {
            await Promise.all([
                GM_setValue(KEY_LIST_RAW, config.listRaw),
                GM_setValue(KEY_LAST_UPDATE, config.lastUpdate)
            ]);
            logger.debug("Liste ve zaman damgası kaydedildi.");
        } catch (err) {
            logger.error("Liste verileri kaydedilemedi:", err);
            showFeedback("Depolama Hatası", "Liste güncellendi ancak kaydedilemedi.", { isError: true });
        }
        return true;
    };

    function applyFilterAction(entry, author) {
        const entryId = entry.dataset.id || 'ID Yok';
        const displayAuthor = entry.dataset.author || author;

        if (entry.querySelector(`.${CSS_PREFIX}opened-warning`)) {
            logger.debug(`Entry #${entryId} manuel açılmış, filtre uygulanmıyor.`);
            return false;
        }

        if (config.filterMode === "hide") {
            if (!entry.classList.contains(`${CSS_PREFIX}hidden`)) {
                entry.classList.add(`${CSS_PREFIX}hidden`);
                logger.debug(`Gizlendi: Entry #${entryId} (Yazar: ${displayAuthor})`);
                return true;
            }
            return false;
        }

        if (entry.classList.contains(`${CSS_PREFIX}collapsed`)) {
             logger.debug(`Entry #${entryId} zaten daraltılmış.`);
             return false;
        }

        const contentEl = entry.querySelector(".content");
        if (!contentEl) {
             logger.warn(`Daraltma: .content bulunamadı: Entry #${entryId}`);
             return false;
        }

        let placeholder = entry.querySelector(`.${CSS_PREFIX}collapse-placeholder`);
        if (!placeholder) {
            placeholder = document.createElement('div');
            placeholder.className = `${CSS_PREFIX}collapse-placeholder`;
            const strongAuthor = document.createElement('strong');
            strongAuthor.textContent = displayAuthor;

            const textSpan = document.createElement('span');
            textSpan.className = `${CSS_PREFIX}collapse-text`;
            textSpan.textContent = 'Listeden: ';
            textSpan.appendChild(strongAuthor);
            textSpan.insertAdjacentText('beforeend', '.');

            placeholder.innerHTML = `<span class="${CSS_PREFIX}collapse-icon" title="Yazar '${displayAuthor}' listede.">🚫</span>`;
            placeholder.appendChild(textSpan);
            placeholder.insertAdjacentHTML('beforeend', `<div class="${CSS_PREFIX}show-link"><a href="#" role="button">Göster</a></div>`);

            const showLink = placeholder.querySelector(`.${CSS_PREFIX}show-link a`);
            showLink?.addEventListener("click", (e) => {
                e.preventDefault();
                e.stopPropagation();
                const currentEntry = e.target.closest('li[data-author]');
                if (!currentEntry) {
                    logger.warn("Genişletme: Üst entry bulunamadı.");
                    return;
                }
                const currentContent = currentEntry.querySelector(".content");
                const currentPlaceholder = currentEntry.querySelector(`.${CSS_PREFIX}collapse-placeholder`);
                const currentAuthor = currentEntry.dataset.author || '?';
                const currentId = currentEntry.dataset.id || '?';

                logger.debug(`Genişletiliyor: Entry #${currentId} (Yazar: ${currentAuthor})`);

                if (currentContent) currentContent.style.display = '';
                if (currentPlaceholder) currentPlaceholder.style.display = 'none';
                currentEntry.classList.remove(`${CSS_PREFIX}collapsed`);

                const footer = currentEntry.querySelector('footer');
                if (footer && !footer.querySelector(`.${CSS_PREFIX}opened-warning`)) {
                    const warningSpan = document.createElement('span');
                    warningSpan.className = `${CSS_PREFIX}opened-warning`;
                    warningSpan.textContent = '⚠️ Filtre Açıldı';
                    warningSpan.title = `'${currentAuthor}' içeriği daraltılmıştı.`;
                    const footerInfo = footer.querySelector('.info .footer-info') || footer.querySelector('.entry-footer-bottom .footer-info') || footer.querySelector('.info') || footer;
                    footerInfo.appendChild(warningSpan);
                 } else if (!footer) {
                     logger.warn(`Genişletilen entry #${currentId} footeri bulunamadı.`);
                 }
            });

            const footerEl = entry.querySelector('footer');
            if (footerEl) {
                 entry.insertBefore(placeholder, footerEl);
            } else if (contentEl) {
                 contentEl.parentNode.insertBefore(placeholder, contentEl.nextSibling);
            } else {
                 entry.appendChild(placeholder);
                 logger.warn(`Entry #${entryId} footer/content bulunamadı, placeholder sona eklendi.`);
            }
        } else {
             placeholder.style.display = 'flex';
        }

        if(contentEl) contentEl.style.display = 'none';
        entry.classList.add(`${CSS_PREFIX}collapsed`);
        logger.debug(`Daraltıldı: Entry #${entryId} (Yazar: ${displayAuthor})`);
        return true;
    }


    function enhanceEntry(entry) {
        if (config.paused) return false;
        if (entry.dataset.eusfProcessed === 'true') return false;
        if (!entry || !entry.matches || !entry.matches('li[data-author]')) {
             if (entry && entry.dataset) entry.dataset.eusfProcessed = 'skipped_invalid_element';
             return false;
        }

        const authorOriginal = entry.dataset.author;
        const authorLower = authorOriginal?.toLowerCase().trim();
        const entryId = entry.dataset.id || 'ID Yok';

        if (!authorLower) {
            logger.warn(`Entry #${entryId} için yazar adı ('data-author') bulunamadı/boş.`);
            entry.dataset.eusfProcessed = 'skipped_empty_author';
            return false;
        }

        entry.dataset.eusfProcessed = 'true';
        let filteredByList = false;

        const firstEntryElement = entryListContainerEl?.firstElementChild;

        try {
            if (filteredAuthorsSet.has(authorLower)) {
                if (!isFirstEntryOnPageProcessed && entry === firstEntryElement) {
                    firstEntryAuthorFilteredOnPage = true;
                    logger.debug(`İlk entry listeye göre filtrelenecek: #${entryId} (Yazar: ${authorOriginal})`);
                }

                if (applyFilterAction(entry, authorLower)) {
                    filteredByList = true;
                    filteredEntryCountOnPage++;
                    config.totalFiltered = (config.totalFiltered || 0) + 1;
                    debouncedSaveTotalFiltered(config.totalFiltered);
                    entry.dataset.eusfAction = `filtered_${config.filterMode}`;
                } else {
                    entry.dataset.eusfAction = 'filter_skipped_or_failed';
                }
            } else {
                 entry.dataset.eusfAction = 'not_in_list';
            }

        } catch (err) {
            logger.error(`Entry #${entryId} işlenemedi (Yazar: ${authorOriginal}):`, err);
            entry.dataset.eusfAction = 'processing_error';
        } finally {
             if (!isFirstEntryOnPageProcessed && entry === firstEntryElement) {
                 isFirstEntryOnPageProcessed = true;
             }
        }
        return filteredByList;
    }

    const debouncedUpdateTopicWarning = debounce(() => {
        const currentEntryListContainer = document.getElementById('entry-item-list');
        const currentTitleH1 = document.getElementById("title");

        if (!currentEntryListContainer || !currentTitleH1) {
            logger.debug("Konu uyarısı: DOM elemanları bulunamadı.");
            return;
        }

        try {
            topicWarningElement?.remove();
            topicWarningElement = null;

            if (!config.showWarning || config.paused || filteredEntryCountOnPage === 0) {
                return;
            }

            const shouldShowWarning = firstEntryAuthorFilteredOnPage || filteredEntryCountOnPage >= TOPIC_WARNING_THRESHOLD;

            if (shouldShowWarning) {
                const targetElement = currentTitleH1.querySelector('a[href^="/entry/"]') || currentTitleH1;

                topicWarningElement = document.createElement("span");
                topicWarningElement.id = `${CSS_PREFIX}title-warning`;
                topicWarningElement.className = `${CSS_PREFIX}topic-warning`;
                topicWarningElement.textContent = "[Filtre Aktif]"; // Normalized text

                let titleText = `${filteredEntryCountOnPage} entry ${config.filterMode === 'hide' ? 'gizlendi' : 'daraltıldı'}.`;
                if (firstEntryAuthorFilteredOnPage) {
                    titleText += " İlk entry de filtrelendi.";
                }
                topicWarningElement.title = titleText;

                targetElement.insertAdjacentElement('beforeend', topicWarningElement);
                logger.debug(`Konu başlığı uyarısı eklendi (${filteredEntryCountOnPage} filtrelendi, ilk: ${firstEntryAuthorFilteredOnPage}).`);
            }
        } catch (err) {
            logger.error("Konu başlığı uyarısı hatası:", err);
            topicWarningElement?.remove();
            topicWarningElement = null;
        }
    }, DEBOUNCE_DELAY_MS);

    function addProfileWarning() {
        if (config.paused || !config.showWarning) {
            logger.debug("Profil uyarısı atlandı.");
            return;
        }

        const authorElement = document.querySelector('h1#user-profile-title');
        if (!authorElement) {
            return;
        }
        logger.debug("Profil sayfası uyarısı kontrol ediliyor...");

        const authorName = authorElement.textContent?.trim();
        if (!authorName) {
            logger.warn("Profil: Yazar adı alınamadı.");
            return;
        }

        const authorLower = authorName.toLowerCase();

        if (filteredAuthorsSet.has(authorLower)) {
            logger.log(`Profildeki yazar "${authorName}" listede.`);

            if (authorElement.querySelector(`.${CSS_PREFIX}profile-warning`)) {
                logger.debug("Profil uyarısı zaten mevcut.");
                return;
            }

            try {
                const warningSpan = document.createElement('span');
                warningSpan.className = `${CSS_PREFIX}profile-warning`;
                warningSpan.textContent = "[Filtre Listesinde]"; // Normalized text
                warningSpan.title = `Yazar (${authorName}) listede, içerikleri ${config.filterMode === 'hide' ? 'gizleniyor' : 'daraltılıyor'}.`;
                authorElement.appendChild(warningSpan);
                logger.log(`Profil sayfasına "${authorName}" uyarısı eklendi.`);
            } catch (err) {
                logger.error(`Profil uyarısı eklenirken hata (${authorName}):`, err);
            }
        } else {
            logger.debug(`Profildeki yazar "${authorName}" listede değil.`);
        }
    }

    let intersectionObserver = null;
    function setupIntersectionObserver() {
        if (!entryListContainerEl) {
             logger.warn("IO: #entry-item-list bulunamadı.");
             return false;
        }

        try {
            intersectionObserver = new IntersectionObserver((entries, observer) => {
                let processedCountInBatch = 0;
                entries.forEach(entry => {
                    if (entry.isIntersecting && entry.target.nodeType === Node.ELEMENT_NODE) {
                        const targetLi = entry.target;
                        if (targetLi.matches('li[data-author]') && targetLi.dataset.eusfProcessed !== 'true') {
                             if (enhanceEntry(targetLi)) {
                                 processedCountInBatch++;
                             }
                             observer.unobserve(targetLi);
                        } else if (targetLi.dataset.eusfProcessed === 'true') {
                             observer.unobserve(targetLi);
                        }
                        else if (!targetLi.matches('li[data-author]')) {
                            observer.unobserve(targetLi);
                        }
                    }
                });

                if (processedCountInBatch > 0) {
                     debouncedUpdateTopicWarning();
                }
            }, {
                root: null,
                rootMargin: '150px 0px 150px 0px',
                threshold: 0.01
             });

            const initialEntries = entryListContainerEl.querySelectorAll(`li[data-author]:not([data-eusf-processed="true"])`);
            logger.log(`IO: ${initialEntries.length} başlangıç entry'si hedefleniyor.`);
            initialEntries.forEach(entry => {
                entry.dataset.eusfObserved = 'true';
                intersectionObserver.observe(entry)
            });

            debouncedUpdateTopicWarning();
            return true;

        } catch (err) {
            logger.error("IO kurulum hatası:", err);
            intersectionObserver = null;
            return false;
        }
    }

    let mutationObserver = null;
    function setupMutationObserver() {
        if (!entryListContainerEl || !intersectionObserver) {
             logger.warn("MO: #entry-item-list veya IO eksik.");
             return false;
        }

        try {
            mutationObserver = new MutationObserver(mutations => {
                let addedToIoCount = 0;
                mutations.forEach(mutation => {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === Node.ELEMENT_NODE) {
                                const entriesToAdd = [];
                                if (node.matches('li[data-author]')) {
                                    entriesToAdd.push(node);
                                }
                                else {
                                    entriesToAdd.push(...node.querySelectorAll('li[data-author]'));
                                }

                                entriesToAdd.forEach(entry => {
                                    if (entry.dataset.eusfProcessed !== 'true' && entry.dataset.eusfObserved !== 'true') {
                                        intersectionObserver.observe(entry);
                                        entry.dataset.eusfObserved = 'true';
                                        addedToIoCount++;
                                    }
                                });
                            }
                        });
                    }
                });
                if (addedToIoCount > 0) {
                    logger.debug(`MO: ${addedToIoCount} yeni entry IO'ya eklendi.`);
                }
            });

            mutationObserver.observe(entryListContainerEl, {
                childList: true,
                subtree: true
            });
            logger.log(`#entry-item-list izleniyor (MO aktif).`);
            return true;

        } catch (err) {
            logger.error("MO kurulum hatası:", err);
            mutationObserver = null;
            return false;
        }
    }

    async function initialize() {
        logger.log(`Script başlatılıyor... v${GM_info?.script?.version || '?'}`);
        await loadConfig();

        entryListContainerEl = document.getElementById('entry-item-list');

        const currentPath = window.location.pathname;

        if (currentPath.startsWith('/biri/')) {
            addProfileWarning();
            logger.log("Profil sayfası işlendi.");
        } else if (entryListContainerEl) {
            logger.log("Entry listesi sayfası.");
            if (!config.paused) {
                 if (setupIntersectionObserver()) {
                     setupMutationObserver();
                 }
            } else {
                logger.log("Script duraklatılmış, Observer'lar kurulmadı.");
            }
        } else {
             logger.log("Entry listesi/profil başlığı bulunamadı.");
        }

        if (!config.paused) {
            const now = Date.now();
            const timeSinceUpdate = now - (config.lastUpdate || 0);
            const needsUpdateCheck = filteredListSize === 0 || timeSinceUpdate > UPDATE_INTERVAL_MS;

            if (needsUpdateCheck) {
                logger.log(`Liste ${filteredListSize === 0 ? 'boş/ilk' : 'güncel değil'} (${Math.round(timeSinceUpdate / (60*60*1000))} saat). Senkronizasyon deneniyor...`);
                syncList(filteredListSize === 0).then(updated => {
                    if (updated) {
                        logger.log("Arka plan liste güncellemesi tamamlandı. Yeni boyut: " + filteredListSize);
                        if (currentPath.startsWith('/biri/')) {
                           addProfileWarning();
                        }
                    } else {
                        logger.log("Arka plan güncellemesi listeyi değiştirmedi/başarısız oldu.");
                    }
                }).catch(err => {
                    logger.error("Arka plan senkronizasyonunda hata:", err);
                });
            } else {
                logger.log(`Liste güncel. Son kontrol: ${config.lastUpdate ? new Date(config.lastUpdate).toLocaleString() : 'Hiç'}.`);
            }

            if (filteredAuthorsSet.size === 0 && !needsUpdateCheck && config.listRaw && config.listRaw.trim().length > 0) {
                logger.warn("Uyarı: Yerel liste var ama ayrıştırma boş!");
            } else if (filteredAuthorsSet.size === 0 && !config.listRaw) {
                logger.warn("Uyarı: Filtre listesi boş!");
            }
        } else {
            logger.log("Filtre duraklatılmış, güncelleme atlandı.");
        }

        registerMenuCommands();

        logger.log(`🎉 ${SCRIPT_NAME} ${config.paused ? 'Duraklatıldı' : 'aktif'}. Mod: ${config.filterMode}.`);
    }

    function registerMenuCommands() {
        const commandIds = [];

        const setConfigAndReload = async (key, value, msg) => {
            try {
                await GM_setValue(key, value);
                config[key] = value;
                showFeedback("Ayar Değişti", msg, { silent: true });
                logger.log(`Ayar: ${key}=${value}. Yenileniyor...`);
                location.reload();
            } catch (err) {
                logger.error(`Ayar (${key}) kaydedilemedi:`, err);
                showFeedback("Depolama Hatası", `Ayar (${key}) kaydedilemedi.\n${err.message}`, { isError: true });
            }
        };

        // Menu commands with normalized text
        commandIds.push(GM_registerMenuCommand(`${config.paused ? "▶️ Aktif Et" : "⏸️ Durdur"}`, () => {
            const newState = !config.paused;
            setConfigAndReload(KEY_PAUSED, newState, `Filtre ${newState ? 'durduruldu' : 'aktif edildi'}. Yenileniyor...`);
        }));

        commandIds.push(GM_registerMenuCommand(`Mod: ${config.filterMode === 'hide' ? 'Gizle' : 'Daralt'} (Değiştir)`, () => {
            const newMode = config.filterMode === 'hide' ? 'collapse' : 'hide';
            setConfigAndReload(KEY_MODE, newMode, `Mod "${newMode === 'hide' ? 'Gizle' : 'Daralt'}" yapıldı. Yenileniyor...`);
        }));

        commandIds.push(GM_registerMenuCommand(`Uyarılar: ${config.showWarning ? "🚫 Gizle" : "⚠️ Göster"}`, () => {
            const newState = !config.showWarning;
            setConfigAndReload(KEY_SHOW_WARNING, newState, `Uyarılar ${newState ? 'gösterilecek' : 'gizlenecek'}. Yenileniyor...`);
        }));

        commandIds.push(GM_registerMenuCommand("🔄 Listeyi Şimdi Güncelle", async () => {
            showFeedback("Güncelleme", "Liste alınıyor...", { silent: true });
            logger.log("Manuel güncelleme başlatıldı...");
            try {
                const updated = await syncList(true);
                if (updated) {
                    showFeedback("Güncelleme Başarılı", `Liste güncellendi (${filteredListSize} yazar). Yenileniyor...`);
                    location.reload();
                } else {
                     logger.warn("Manuel güncelleme: Liste değişmedi/hata.");
                     showFeedback("Güncelleme Sonucu", "Liste güncellenemedi/değişmedi. Konsolu kontrol edin.", { isError: (filteredListSize === 0 && !config.listRaw) });
                }
            } catch (err) {
                 logger.error("Manuel güncelleme hatası:", err);
                 showFeedback("Güncelleme Hatası", `Hata: ${err.message}`, { isError: true });
            }
        }));

        commandIds.push(GM_registerMenuCommand(`📊 İstatistikler`, async () => {
            const total = await GM_getValue(KEY_TOTAL_FILTERED, config.totalFiltered);
            const lastUpdateDate = config.lastUpdate ? new Date(config.lastUpdate).toLocaleString("tr-TR") : "Hiç";
            const statsText = `Toplam Filtrelenen: ${total}\n`
                            + `Liste Boyutu: ${filteredListSize}\n`
                            + `Son Güncelleme: ${lastUpdateDate}\n`
                            + `Durum: ${config.paused ? 'Duraklatıldı' : 'Aktif'}\n`
                            + `Mod: ${config.filterMode === 'hide' ? 'Gizle' : 'Daralt'}\n`
                            + `Uyarılar: ${config.showWarning ? 'Açık' : 'Kapalı'}`;
            showFeedback("Filtre İstatistikleri", statsText);
        }));


        commandIds.push(GM_registerMenuCommand(`🗑️ Ayarları ve Önbelleği Sıfırla`, async () => {
             if (confirm(`[${SCRIPT_NAME}] Emin misiniz?\n\nTüm ayarları ve yerel filtre listesi önbelleğini sıfırlayacak.\n\nSayfa yenilendikten sonra liste tekrar indirilecek.`)) {
                 logger.warn("Kullanıcı sıfırlamayı onayladı.");
                 try {
                     const keysToDelete = [
                         KEY_PAUSED, KEY_MODE, KEY_SHOW_WARNING, KEY_LIST_RAW,
                         KEY_LAST_UPDATE, KEY_TOTAL_FILTERED
                     ];
                     const results = await Promise.allSettled(keysToDelete.map(key => GM_deleteValue(key)));

                     results.forEach((result, index) => {
                         if (result.status === 'rejected') {
                             logger.error(`'${keysToDelete[index]}' silinirken hata:`, result.reason);
                         }
                     });

                     filteredAuthorsSet = new Set();
                     filteredListSize = 0;
                     config = { paused: false, filterMode: 'collapse', showWarning: true, listRaw: '', lastUpdate: 0, totalFiltered: 0 };

                     showFeedback("Sıfırlandı", "Ayarlar ve önbellek temizlendi. Yenileniyor...");
                     location.reload();
                 } catch (err) {
                      logger.error("Sıfırlama hatası:", err);
                      showFeedback("Sıfırlama Hatası", `Hata: ${err.message}`, { isError: true });
                 }
             } else {
                 logger.log("Kullanıcı sıfırlamayı iptal etti.");
                 showFeedback("İptal Edildi", "Sıfırlama iptal edildi.", { silent: true });
             }
        }));

        logger.debug(`${commandIds.length} menü komutu kaydedildi.`);
    }

    try {
         initialize().catch(err => {
            logger.error("Başlatma hatası (initialize promise):", err);
            showFeedback("Başlatma Başarısız", `Hata:\n${err.message}\nScript çalışmayabilir.`, { isError: true });
        });
    } catch (err) {
        logger.error("Başlatma senkron hatası:", err);
        showFeedback("Başlatma Hatası", `Senkron hata:\n${err.message}\nScript çalışamayabilir.`, { isError: true });
    }

})();