Ekşi Author Filter

Filters entries from specified authors on Ekşi Sözlük, loaded from a remote list.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Ekşi Author Filter
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Filters entries from specified authors on Ekşi Sözlük, loaded from a remote list.
// @author       Your Name // Added placeholder
// @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/
// @exclude      *://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 // Made specific, removed * and gist
// @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';

    // --- Constants ---
    const SCRIPT_NAME = "Ekşi Author Filter"; // Simplified name
    const AUTHOR_LIST_URL = "https://raw.githubusercontent.com/bat9254/troll-list/refs/heads/main/list.txt"; // Removed REPLACE comment
    const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
    const NETWORK_TIMEOUT_MS = 20000; // 20s
    const LOG_PREFIX = `[${SCRIPT_NAME}]`;
    const DEBOUNCE_DELAY_MS = 250;
    const TOPIC_WARNING_THRESHOLD = 3; // Minimum filtered entries (excluding first) to show warning
    const CSS_PREFIX = "efh-"; // Keep prefix for isolation

    // --- Storage Keys ---
    const KEY_PAUSED = "efh_paused_v2";
    const KEY_MODE = "efh_filterMode_v2"; // 'hide' or 'collapse'
    const KEY_SHOW_WARNING = "efh_showTopicWarning_v2";
    const KEY_LIST_RAW = "efh_authorListRaw_v2";
    const KEY_LAST_UPDATE = "efh_lastUpdateTime_v2";
    const KEY_TOTAL_FILTERED = "efh_totalFiltered_v2";

    // --- CSS ---
    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; } /* Changed cursor */
        .${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; } /* Added cursor */
        .${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}show-link { font-style:normal; flex-shrink:0; margin-left:auto; }
        .${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; }
        .${CSS_PREFIX}show-link a::before { content:"» "; opacity:0.7; }
        .${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}hidden { display: none !important; }
        .${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; } /* Added cursor */
    `);

    // --- Helpers ---
    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), // Keep debug for development
    };
    const debounce = (func, wait) => {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    };

    // --- GM API Check ---
    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 = `KRİTİK HATA: Gerekli Tampermonkey API fonksiyonları eksik: ${missing.join(', ')}! Script ÇALIŞMAYACAK. Lütfen Tampermonkey'in güncel olduğundan ve script'e yetki verildiğinden emin olun.`;
        logger.error(errorMsg);
        alert(`${SCRIPT_NAME} - KRİTİK HATA:\n${errorMsg}`);
        return; // Stop execution
    }

    // --- Feedback ---
    function showFeedback(title, text, options = {}) {
        const { isError = false, silent = false } = options;
        const prefix = isError ? "HATA" : "BİLGİ";
        (isError ? logger.error : logger.log)(`${prefix}: ${title}`, text); // Log clearly
        if (!silent) {
            alert(`[${SCRIPT_NAME}] ${prefix}: ${title}\n\n${text}`);
        }
    }

    // --- State ---
    let config = {};
    let filteredAuthorsSet = new Set();
    let filteredListSize = 0;
    let filteredEntryCountOnPage = 0; // Renamed for clarity
    let firstEntryAuthorFilteredOnPage = false; // Renamed for clarity
    let topicWarningElement = null;

    // --- Config Load ---
    async function loadConfig() {
        logger.debug("Yapılandırma yükleniyor...");
        try {
            // Use Promise.all for parallel fetching
            const [paused, filterMode, showWarning, listRaw, lastUpdate, totalFiltered] = await Promise.all([
                GM_getValue(KEY_PAUSED, false),
                GM_getValue(KEY_MODE, "collapse"), // Default to collapse
                GM_getValue(KEY_SHOW_WARNING, true),
                GM_getValue(KEY_LIST_RAW, ""),
                GM_getValue(KEY_LAST_UPDATE, 0),
                GM_getValue(KEY_TOTAL_FILTERED, 0)
            ]);
            config = { paused, filterMode, showWarning, listRaw, lastUpdate, totalFiltered };
            filteredAuthorsSet = parseAuthorList(config.listRaw);
            filteredListSize = filteredAuthorsSet.size;
            logger.log(`Yapılandırma yüklendi. Durum: ${config.paused ? 'DURAKLATILDI' : 'AKTİF'}, Mod: ${config.filterMode}, Liste Boyutu: ${filteredListSize}, Toplam Filtrelenen: ${config.totalFiltered}`);
        } catch (err) {
            logger.error("Yapılandırma YÜKLENEMEDİ:", err);
            // Set defaults safely
            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 });
        }
    }

    // --- Core Functions ---
    const fetchList = () => new Promise((resolve, reject) => {
        logger.debug(`Liste isteniyor: ${AUTHOR_LIST_URL}`);
        GM_xmlhttpRequest({
            method: "GET",
            url: AUTHOR_LIST_URL,
            timeout: NETWORK_TIMEOUT_MS,
            responseType: 'text',
            headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' }, // Stronger cache control
            onload: response => {
                if (response.status >= 200 && response.status < 300) {
                    logger.debug(`Liste başarıyla alındı (HTTP ${response.status}). Boyut: ${response.responseText.length} bytes.`);
                    resolve(response.responseText);
                } else {
                    logger.warn(`Liste alınamadı. Sunucu yanıtı: HTTP ${response.status} ${response.statusText}`);
                    reject(new Error(`HTTP ${response.status} ${response.statusText || 'Error'}`));
                }
            },
            onerror: response => {
                logger.error("Liste çekme sırasında ağ hatası:", response.statusText || 'Bilinmeyen ağ hatası', response);
                reject(new Error(`Ağ Hatası: ${response.statusText || 'Bilinmeyen'}`));
            },
            ontimeout: () => {
                logger.error(`Liste çekme zaman aşımına uğradı (${NETWORK_TIMEOUT_MS / 1000}s).`);
                reject(new Error(`Zaman Aşımı (${NETWORK_TIMEOUT_MS / 1000}s)`));
            }
        });
    });

    const parseAuthorList = (rawText) => {
        if (!rawText || typeof rawText !== 'string') {
            logger.warn("Ayrıştırılacak liste metni boş veya geçersiz.");
            return new Set();
        }
        try {
            const authors = rawText.split(/[\r\n]+/) // Handles different line endings
                .map(line => line.replace(/#.*$/, '').trim().toLowerCase()) // Remove comments, trim, lowercase
                .filter(line => line.length > 0); // Remove empty lines
            logger.debug(`Liste ayrıştırıldı, ${authors.length} potansiyel yazar bulundu.`);
            return new Set(authors);
        } catch (err) {
            logger.error("Liste AYRIŞTIRILAMADI:", err);
            showFeedback("Liste Ayrıştırma Hatası", `İndirilen liste işlenemedi. Hata: ${err.message}`, { isError: true });
            return new Set(); // Return empty set on error
        }
    };

    const syncList = async (force = false) => {
        logger.log(`Liste güncellemesi ${force ? 'ZORLANIYOR' : 'kontrol ediliyor'}...`);
        let newRawText;
        try {
            newRawText = await fetchList();
        } catch (err) {
            logger.error("Liste ÇEKME hatası:", err.message);
            // Only show alert if forced or if we have no list at all
            if (force || filteredListSize === 0) {
                showFeedback("Güncelleme Başarısız", `Filtre listesi uzak sunucudan alınamadı.\n\nHata: ${err.message}\n\nMevcut liste (varsa) kullanılmaya devam edecek.`, { isError: true });
            }
            return false; // Indicate failure
        }

        // Check if list content actually changed (or forced update)
        if (!force && config.listRaw === newRawText) {
            logger.log("Liste içeriği değişmemiş. Güncelleme atlanıyor.");
            // Still update timestamp if check was successful
            config.lastUpdate = Date.now();
            await GM_setValue(KEY_LAST_UPDATE, config.lastUpdate).catch(e => logger.error("Zaman damgası kaydı (değişiklik yokken) başarısız:", e));
            return false; // Not updated
        }

        logger.log(force ? "Zorunlu güncelleme veya liste içeriği değişmiş, işleniyor." : "Liste değişmiş, güncelleniyor.");
        let newListSet;
        try {
            newListSet = parseAuthorList(newRawText);
        } catch (err) {
            // Error already shown by parseAuthorList
            logger.error("Liste işleme hatası (syncList içinde yakalandı):", err);
            // Do not proceed with saving corrupted data, keep old list
            return false;
        }

        // Sanity check: If we had a list before, and the new list is empty BUT the raw text wasn't, something is wrong.
        if (filteredListSize > 0 && newListSet.size === 0 && newRawText.trim().length > 0) {
            logger.warn("Yeni liste verisi alındı ancak ayrıştırma sonucu BOŞ! Bu beklenmedik bir durum. Güvenlik için eski liste korunuyor. Lütfen listeyi kontrol edin:", AUTHOR_LIST_URL);
            showFeedback("Güncelleme Uyarısı", "Yeni liste indirildi ancak işlenince boş sonuç verdi. Eski liste kullanılıyor.", { isError: true });
            // Update timestamp to prevent constant retries if the remote list is broken
            config.lastUpdate = Date.now();
            await GM_setValue(KEY_LAST_UPDATE, config.lastUpdate).catch(e=>logger.error("Zaman damgası kaydı (ayrıştırma hatası sonrası) başarısız:", e));
            return false; // Not updated
        }

        // Looks good, proceed with update
        const oldSize = filteredListSize;
        filteredAuthorsSet = newListSet;
        filteredListSize = filteredAuthorsSet.size;
        config.listRaw = newRawText;
        config.lastUpdate = Date.now();
        logger.log(`Liste başarıyla güncellendi. Eski boyut: ${oldSize}, Yeni boyut: ${filteredListSize}`);

        try {
            await Promise.all([
                GM_setValue(KEY_LIST_RAW, config.listRaw),
                GM_setValue(KEY_LAST_UPDATE, config.lastUpdate)
            ]);
            logger.debug("Güncel liste ve zaman damgası başarıyla kaydedildi.");
        } catch (err) {
            logger.error("Güncel liste verileri KAYDEDİLEMEDİ:", err);
            showFeedback("Depolama Hatası", "Liste güncellendi ancak yerel olarak kaydedilemedi. Sayfa yenilendiğinde eski liste yüklenebilir.", { isError: true });
            // Data is updated in memory, but storage failed. Return true as it was updated.
        }
        return true; // Updated successfully
    };

    function applyFilterAction(entry, author) {
        const entryId = entry.dataset.id || 'ID Yok'; // Use original author case for display
        const displayAuthor = entry.dataset.author || author; // Fallback just in case

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

        // --- Collapse Mode ---
        if (entry.classList.contains(`${CSS_PREFIX}collapsed`)) {
             logger.debug(`Zaten daraltılmış: Entry #${entryId}`);
             return 'already_collapsed'; // Already processed and collapsed
        }

        // Check if placeholder already exists (e.g., from a previous run before reload)
        let placeholder = entry.querySelector(`.${CSS_PREFIX}collapse-placeholder`);
        const contentEl = entry.querySelector(".content"); // Check content existence early

        if (!contentEl) {
             logger.warn(`Daraltma başarısız (içerik bulunamadı): Entry #${entryId}`);
             return 'collapse_failed_no_content';
        }

        if (!placeholder) {
            placeholder = document.createElement('div');
            placeholder.className = `${CSS_PREFIX}collapse-placeholder`;
            // Use displayAuthor for user-facing text
            placeholder.innerHTML = `<span class="${CSS_PREFIX}collapse-icon" title="Yazar '${displayAuthor}' filtre listesinde.">🚫</span><span class="${CSS_PREFIX}collapse-text">Filtrelenen yazar: <strong>${displayAuthor}</strong>.</span><div class="${CSS_PREFIX}show-link"><a href="#" role="button">Göster</a></div>`;

            const showLink = placeholder.querySelector(`.${CSS_PREFIX}show-link a`);
            if (showLink) {
                showLink.addEventListener("click", (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    // Find elements again within the current scope
                    const currentEntry = e.target.closest('li[data-author]');
                    if (!currentEntry) return; // Should not happen
                    const currentContent = currentEntry.querySelector(".content");
                    const currentPlaceholder = currentEntry.querySelector(`.${CSS_PREFIX}collapse-placeholder`);

                    if (currentContent) currentContent.style.display = ''; // Restore display
                    if (currentPlaceholder) currentPlaceholder.style.display = 'none'; // Hide placeholder instead of removing
                    currentEntry.classList.remove(`${CSS_PREFIX}collapsed`);

                    // Add warning to footer
                    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ı'; // Clarified text
                        warningSpan.title = `'${displayAuthor}' yazarına ait bu içerik filtre nedeniyle daraltılmıştı.`;
                        // Append to a specific container within footer if possible, otherwise just footer
                        const footerInfo = footer.querySelector('.info') || footer;
                        footerInfo.appendChild(warningSpan);
                    }
                     logger.debug(`Genişletildi: Entry #${currentEntry.dataset.id} (Yazar: ${displayAuthor})`);
                }); // Removed { once: true } to allow re-collapsing if needed (though UI doesn't support it now)
            }

            // Insert placeholder before footer or content
            const footerEl = entry.querySelector('footer');
             if (footerEl) {
                entry.insertBefore(placeholder, footerEl);
            } else if (contentEl) {
                 entry.insertBefore(placeholder, contentEl.nextSibling); // Insert after content if no footer
            } else {
                entry.appendChild(placeholder); // Fallback: append
            }

        } else {
            // Placeholder exists, just make sure it's visible
             placeholder.style.display = 'flex';
        }

         // Hide content only if we successfully prepared the placeholder
         if(contentEl) contentEl.style.display = 'none';

        entry.classList.add(`${CSS_PREFIX}collapsed`);
        logger.debug(`Daraltıldı: Entry #${entryId} (Yazar: ${displayAuthor})`);
        return 'collapse';
    }

    function enhanceEntry(entry, isFirstOnPage = false) {
        if (entry.dataset.efhProcessed === 'true') {
            // logger.debug(`Zaten işlenmiş: Entry #${entry.dataset.id}`);
            return; // Already processed
        }
        if (!entry.matches('li[data-author]')) {
             entry.dataset.efhProcessed = 'skipped_no_author_attr'; // Mark as skipped
             return; // Not a valid entry element
        }

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

        if (!authorLower) {
            logger.warn(`Entry #${entryId} 'data-author' attribute'üne sahip ancak değeri boş.`);
            entry.dataset.efhProcessed = 'skipped_empty_author'; // Mark as skipped
            return; // Skip if no author identifier
        }

        let action = 'none';
        try {
            if (config.paused) {
                action = 'paused';
            } else if (filteredAuthorsSet.has(authorLower)) {
                if (isFirstOnPage) {
                    firstEntryAuthorFilteredOnPage = true; // Set page flag
                    logger.debug(`Sayfanın ilk entry'si filtrelenecek: #${entryId} (Yazar: ${entry.dataset.author})`);
                 }
                filteredEntryCountOnPage++; // Increment page counter
                action = applyFilterAction(entry, authorLower); // Pass lowercase author

                // Update total count (fire and forget, with check)
                 if (action === 'hide' || action === 'collapse') {
                    config.totalFiltered = (config.totalFiltered || 0) + 1;
                    GM_setValue(KEY_TOTAL_FILTERED, config.totalFiltered)
                        .then(() => logger.debug(`Toplam filtreleme sayısı güncellendi: ${config.totalFiltered}`))
                        .catch(err => logger.warn("Toplam sayaç kaydedilemedi:", err));
                 }

            } else {
                action = 'not_in_list';
            }
        } catch (err) {
            logger.error(`Entry #${entryId} İŞLENEMEDİ (Yazar: ${entry.dataset.author}):`, err);
            action = 'error';
        } finally {
             // Always mark as processed (or skipped) to avoid re-processing in this session
             entry.dataset.efhProcessed = 'true';
             entry.dataset.efhAction = action; // Store action taken for debugging
        }
    }

    const updateTopicWarning = () => {
        try {
            // Remove existing warning first
            topicWarningElement?.remove();
            topicWarningElement = null;

            if (!config.showWarning || config.paused || filteredEntryCountOnPage === 0) {
                 // logger.debug("Konu başlığı uyarısı gösterilmeyecek (Kapalı, Duraklatılmış veya Filtre Yok).");
                 return; // Don't show if disabled, paused, or nothing filtered on page
            }

            const titleH1 = document.getElementById("title");
            if (!titleH1) {
                logger.warn("Konu başlığı elementi (#title) bulunamadı.");
                return; // Cannot add warning if title element is missing
            }
            // Prefer appending to the link inside h1 if it exists
            const targetElement = titleH1.querySelector('a[href^="/entry/"]') || titleH1;

            // Determine if warning should be shown based on count or if first entry hit
            // Show if first entry is filtered OR if the count exceeds the threshold
            const showWarning = firstEntryAuthorFilteredOnPage || filteredEntryCountOnPage > TOPIC_WARNING_THRESHOLD;

            if (showWarning) {
                topicWarningElement = document.createElement("span");
                topicWarningElement.id = `${CSS_PREFIX}title-warning`;
                topicWarningElement.className = `${CSS_PREFIX}topic-warning`;
                topicWarningElement.textContent = "[Yazar Filtresi Aktif]"; // Keep it simple

                // Generate a more informative title attribute (tooltip)
                let titleText = `Bu sayfada ${filteredEntryCountOnPage} entry filtrelendi (${config.filterMode === 'hide' ? 'gizlendi' : 'daraltıldı'}).`;
                if (firstEntryAuthorFilteredOnPage) {
                    titleText += " Sayfanın ilk entry'si de filtrelendi.";
                } else if (filteredEntryCountOnPage <= TOPIC_WARNING_THRESHOLD) {
                     // This case should technically not happen due to showWarning logic, but added for robustness
                     titleText += ` (Uyarı ${TOPIC_WARNING_THRESHOLD} filtreden sonra gösterilir.)`;
                }
                topicWarningElement.title = titleText;

                // Append the warning element
                targetElement.insertAdjacentElement('beforeend', topicWarningElement); // Append after the title text/link
                logger.debug("Konu başlığı uyarısı eklendi.");
            } else {
                 // logger.debug(`Konu başlığı uyarısı gösterilmedi (filtrelenen: ${filteredEntryCountOnPage}, ilk entry: ${firstEntryAuthorFilteredOnPage}, eşik: ${TOPIC_WARNING_THRESHOLD})`);
            }
        } catch (err) {
            logger.error("Konu başlığı uyarısı eklenirken HATA oluştu:", err);
            topicWarningElement?.remove(); // Clean up if error occurred during creation/insertion
            topicWarningElement = null;
        }
    };

    const processVisibleEntries = debounce(() => {
        logger.debug("Görünürdeki entry'ler işleniyor (debounce)...");
        filteredEntryCountOnPage = 0; // Reset page counter for this run
        firstEntryAuthorFilteredOnPage = false; // Reset page flag for this run

        // Select only unprocessed entries with the data-author attribute within the list
        const selector = '#entry-item-list > li[data-author]:not([data-efh-processed]), #entry-item-list > li[data-author][data-efh-processed^="skipped"]';
        const entries = document.querySelectorAll(selector);

        if (entries.length > 0) {
            logger.log(`${entries.length} yeni/işlenmemiş entry bulundu. İşleniyor...`);

            // Check if the very first entry on the page (overall first child) needs processing
            const pageFirstEntryElement = document.querySelector('#entry-item-list > li:first-child');
            const isFirstEntryOnPageOverall = (el) => el === pageFirstEntryElement;

            entries.forEach(entry => {
                // Pass true if this entry is the very first one on the entire page list
                enhanceEntry(entry, isFirstEntryOnPageOverall(entry));
            });

            updateTopicWarning(); // Update warning after processing the batch
            logger.log(`Bu işlem döngüsü tamamlandı. Bu sayfada toplam ${filteredEntryCountOnPage} entry filtrelendi.`);
        } else {
            logger.debug("İşlenecek yeni entry bulunamadı.");
             // Re-run warning update in case some entries were un-hidden manually (though this doesn't reset counts)
             updateTopicWarning();
        }
    }, DEBOUNCE_DELAY_MS);

    // --- Init ---
    async function initialize() {
        logger.log(`Script BAŞLATILIYOR... v${GM_info?.script?.version || '?'}`);
        await loadConfig(); // Load config first

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

            if (needsUpdate) {
                logger.log(`Liste ${filteredListSize === 0 ? 'boş' : 'güncel değil'} (${Math.round(timeSinceUpdate / 3600000)} saat önce). Arka planda senkronizasyon deneniyor...`);
                // Start sync but don't wait for it to finish before initial processing
                syncList(filteredListSize === 0).then(updated => {
                    if (updated) {
                        listUpdatedInBackground = true;
                        logger.log("Arka plan liste güncellemesi TAMAMLANDI. Yeni boyut: " + filteredListSize);
                        // Re-process entries as the list has changed
                        processVisibleEntries();
                    } else {
                        logger.log("Arka plan liste güncellemesi sonucu liste değişmedi veya başarısız oldu.");
                    }
                }).catch(err => {
                    // Error is already logged within syncList
                    logger.error("Arka plan senkronizasyonunda yakalanamayan hata:", err);
                });
            } else {
                logger.log(`Liste güncel görünüyor (Son güncelleme ${Math.round(timeSinceUpdate / 3600000)} saat önce).`);
            }
            if (filteredAuthorsSet.size === 0 && !needsUpdate) {
                // Only warn if not attempting an update
                logger.warn("UYARI: Filtre listesi boş veya yüklenemedi!");
            }
        } else {
            logger.log("Filtre başlangıçta DURAKLATILMIŞ.");
        }

        // Initial processing immediately after load (might use old list if sync is pending)
        logger.debug("İlk entry işleme başlatılıyor...");
        processVisibleEntries();

        // Observer Setup
        const entryListContainer = document.querySelector('#entry-item-list');
        if (entryListContainer) {
            try {
                const observer = new MutationObserver(mutations => {
                    // Check if any added nodes are relevant list items
                    const hasRelevantAdditions = mutations.some(m =>
                        m.type === 'childList' && m.addedNodes.length > 0 &&
                        Array.from(m.addedNodes).some(n =>
                            n.nodeType === Node.ELEMENT_NODE &&
                            (n.matches?.('li[data-author]') || n.querySelector?.('li[data-author]')) // Check node itself or children
                        )
                    );

                    if (hasRelevantAdditions) {
                        logger.debug("Observer: Yeni entry(ler) içeren değişiklik tespit edildi.");
                        processVisibleEntries(); // Trigger debounced processing
                    }
                });
                observer.observe(entryListContainer, { childList: true, subtree: true }); // Watch for added nodes anywhere in the list container
                logger.log("#entry-item-list başarıyla İZLENİYOR.");
            } catch (err) {
                logger.error("MutationObserver BAŞLATILAMADI:", err);
                showFeedback("Kritik Hata", "Sayfa değişiklikleri (yeni entry'ler) otomatik olarak İZLENEMİYOR! Sayfayı yenilemeniz gerekebilir.", { isError: true });
            }
        } else {
            logger.warn("#entry-item-list BULUNAMADI! Sayfa yapısı değişmiş olabilir. Dinamik olarak yüklenen entry'ler işlenemeyecek.");
        }

        registerMenuCommands(); // Setup menu commands
        logger.log(`🎉 ${SCRIPT_NAME} başarıyla yüklendi ve aktif.`);
    }

    // --- Menu ---
    function registerMenuCommands() {
        // Helper to set config and reload
        const setConfigAndReload = async (key, value, msg) => {
            try {
                await GM_setValue(key, value);
                config[key] = value; // Update in-memory config as well
                showFeedback("Ayar Değiştirildi", msg, { silent: true }); // Silent alert before reload
                logger.log(`Ayar değiştirildi (${key}=${value}). Sayfa yenileniyor...`);
                location.reload();
            } catch (err) {
                showFeedback("Depolama Hatası", `Ayar (${key}) kaydedilemedi.\n${err.message}`, { isError: true });
                registerMenuCommands(); // Re-register to show the *old* state if save failed
            }
        };

        // Clear existing commands before registering new ones (useful for updates/debugging)
        // Note: Tampermonkey typically handles this, but explicit removal can be safer in some contexts.
        // However, GM_unregisterMenuCommand is not standard/widely supported. We rely on Tampermonkey's overwrite.

        GM_registerMenuCommand(`${config.paused ? "▶️ Filtreyi AKTİF ET" : "⏸️ Filtreyi DURDUR"}`, () => {
            setConfigAndReload(KEY_PAUSED, !config.paused, `Filtre ${!config.paused ? 'AKTİF' : 'DURDU'}. Sayfa yenileniyor...`);
        });

        GM_registerMenuCommand(`Mod: ${config.filterMode === 'hide' ? 'Gizle' : 'Daralt'} (Değiştirmek için tıkla)`, () => {
            const newMode = config.filterMode === 'hide' ? 'collapse' : 'hide';
            setConfigAndReload(KEY_MODE, newMode, `Filtreleme modu "${newMode === 'hide' ? 'Gizle' : 'Daralt'}" olarak ayarlandı. Sayfa yenileniyor...`);
        });

        GM_registerMenuCommand(`Başlık Uyarısını ${config.showWarning ? "🚫 Gizle" : "⚠️ Göster"}`, () => {
            setConfigAndReload(KEY_SHOW_WARNING, !config.showWarning, `Konu başlığı uyarısı ${!config.showWarning ? 'gösterilecek' : 'gizlenecek'}. Sayfa yenileniyor...`);
        });

        GM_registerMenuCommand("🔄 Listeyi ŞİMDİ Güncelle", async () => {
            showFeedback("Güncelleme", "Liste uzak sunucudan alınıyor...", { silent: true }); // Start feedback silently
            const updated = await syncList(true); // Force update
            if (updated) {
                showFeedback("Başarılı", `Liste güncellendi (${filteredListSize} yazar). Değişikliklerin uygulanması için sayfa yenileniyor...`);
                location.reload(); // Reload on successful update
            } else {
                // Provide non-silent feedback if update failed or list was same
                 showFeedback("Güncelleme Sonucu", "Liste güncellenemedi (hata oluştu veya liste zaten günceldi). Daha fazla bilgi için konsolu kontrol edin.", { isError: filteredListSize === 0 }); // Show error if list remains empty
            }
        });

        GM_registerMenuCommand(`📊 Filtre İstatistikleri`, async () => {
            // Re-read total filtered count for accuracy, others are likely up-to-date in memory
            const total = await GM_getValue(KEY_TOTAL_FILTERED, config.totalFiltered);
            const lastUpdateDate = config.lastUpdate ? new Date(config.lastUpdate).toLocaleString("tr-TR") : "Hiç";
            const statsText = `Genel Toplam Filtrelenen: ${total}\n`
                            + `Mevcut Liste Boyutu: ${filteredListSize}\n`
                            + `Son Liste Kontrolü/Güncellemesi: ${lastUpdateDate}\n`
                            + `Filtre Durumu: ${config.paused ? 'DURAKLATILDI' : 'AKTİF'}\n`
                            + `Mod: ${config.filterMode === 'hide' ? 'Gizle' : 'Daralt'}`;
            showFeedback("İstatistikler", statsText);
        });

        GM_registerMenuCommand(`🗑️ Önbelleği ve Ayarları Sıfırla`, async () => {
             if (confirm(`[${SCRIPT_NAME}] Emin misiniz?\n\nBu işlem TÜM AYARLARI (durum, mod, uyarı) ve yerel liste önbelleğini SIFIRLAR. Sayfa yenilendikten sonra liste tekrar indirilecektir.`)) {
                 logger.warn("Kullanıcı önbelleği ve ayarları sıfırlamayı seçti.");
                 try {
                     // List all keys to delete
                     const keysToDelete = [KEY_PAUSED, KEY_MODE, KEY_SHOW_WARNING, KEY_LIST_RAW, KEY_LAST_UPDATE, KEY_TOTAL_FILTERED];
                     await Promise.all(keysToDelete.map(key => GM_deleteValue(key)));

                     // Reset in-memory state immediately
                     filteredAuthorsSet = new Set();
                     filteredListSize = 0;
                     config = { paused: false, filterMode: 'collapse', showWarning: true, listRaw: '', lastUpdate: 0, totalFiltered: 0 }; // Reset to defaults

                     showFeedback("Başarılı", "Tüm ayarlar ve önbellek temizlendi. Varsayılan ayarlara dönüldü. Sayfa yenileniyor...");
                     location.reload();
                 } catch (err) {
                      logger.error("Önbellek/Ayarlar temizlenirken HATA:", err);
                      showFeedback("Hata", `Önbellek ve ayarlar temizlenemedi: ${err.message}`, { isError: true });
                 }
             } else {
                 showFeedback("İptal", "Sıfırlama işlemi iptal edildi.", { silent: true });
             }
        });
    }

    // --- Start ---
    // Wrap initialization in a try-catch for ultimate safety
    try {
         initialize().catch(err => { // Catch potential errors within the async initialize function itself
            logger.error("Başlatma sırasında KRİTİK HATA (initialize içinde yakalandı):", err);
            alert(`[${SCRIPT_NAME}] BAŞLATMA BAŞARISIZ!\n\nHata: ${err.message}\n\nDetaylar için konsolu (F12) kontrol edin.`);
        });
    } catch (err) { // Catch potential errors during the setup before initialize() is even called
        logger.error("Başlatma sırasında KRİTİK HATA (initialize dışında yakalandı):", err);
        alert(`[${SCRIPT_NAME}] KRİTİK BAŞLATMA HATASI!\n\nHata: ${err.message}\n\nScript çalışamayabilir.`);
    }

})();
// --- END ---