Ekşi Author Filter

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

目前為 2025-04-14 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Ekşi Author Filter
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Filters entries from specified authors on Ekşi Sözlük, loaded from a remote list.
// @author      
// @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
// @connect      gist.githubusercontent.com
// @connect      *
// @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 (Mini)";
    const AUTHOR_LIST_URL = "https://raw.githubusercontent.com/unless7146/stardust3903/main/173732994.txt"; // !!! REPLACE !!!
    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;
    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:default; font-weight:bold; }
        .${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; }
        .${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:default; display:inline-block; font-style:normal; font-weight:bold; }
    `);

    // --- 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),
    };
    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.`;
        logger.error(errorMsg); alert(`${SCRIPT_NAME} - KRİTİK HATA:\n${errorMsg}`); return;
    }

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

    // --- State ---
    let config = {};
    let filteredAuthorsSet = new Set();
    let filteredListSize = 0;
    let filteredEntryCount = 0;
    let firstEntryAuthorFiltered = false;
    let topicWarningElement = null;

    // --- Config Load ---
    async function loadConfig() {
        logger.debug("Yapılandırma yükleniyor...");
        try {
            const [paused, filterMode, showWarning, listRaw, lastUpdate, totalFiltered] = await Promise.all([
                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)
            ]);
            config = { paused, filterMode, showWarning, listRaw, lastUpdate, totalFiltered };
            filteredAuthorsSet = parseAuthorList(config.listRaw); // Parse raw list directly
            filteredListSize = filteredAuthorsSet.size;
            logger.log(`Yapılandırma yüklendi. Filtre: ${config.paused ? 'DURDU' : 'AKTİF'}, Mod: ${config.filterMode}, Liste Boyutu: ${filteredListSize}`);
        } catch (err) {
            logger.error("Yapılandırma YÜKLENEMEDİ:", 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 });
        }
    }

    // --- Core Functions ---
    const fetchList = () => new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: "GET", url: AUTHOR_LIST_URL, timeout: NETWORK_TIMEOUT_MS, responseType: 'text',
            headers: { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Expires': '0' },
            onload: r => (r.status >= 200 && r.status < 300) ? resolve(r.responseText) : reject(new Error(`HTTP ${r.status}`)),
            onerror: r => reject(new Error(`Ağ Hatası: ${r.statusText || 'Bilinmeyen'}`)),
            ontimeout: () => reject(new Error(`Zaman Aşımı (${NETWORK_TIMEOUT_MS / 1000}s)`))
        });
    });

    const parseAuthorList = (rawText) => {
        if (!rawText || typeof rawText !== 'string') return new Set();
        try {
            return new Set(rawText.split(/[\r\n]+/)
                .map(line => line.trim().toLowerCase())
                .filter(line => line && !line.startsWith("#")));
        } catch (err) {
            logger.error("Liste AYRIŞTIRILAMADI:", err); return new Set();
        }
    };

    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);
            if (force || filteredListSize === 0) showFeedback("Güncelleme Başarısız", `Liste alınamadı.\n${err.message}`, { isError: true });
            return false;
        }

        try {
            if (force || config.listRaw !== newRawText) {
                logger.log(force ? "Zorunlu güncelleme." : "Liste değişmiş, güncelleniyor.");
                const newListSet = parseAuthorList(newRawText);

                if (filteredListSize > 0 && newListSet.size === 0 && newRawText.length > 0) {
                    logger.warn("Yeni veri var ama ayrıştırma sonucu boş! Eski liste korunuyor.");
                    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;
                }

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

                await Promise.all([
                    GM_setValue(KEY_LIST_RAW, config.listRaw),
                    GM_setValue(KEY_LAST_UPDATE, config.lastUpdate)
                ]).catch(err => {
                    logger.error("Güncel liste verileri KAYDEDİLEMEDİ:", err);
                    showFeedback("Depolama Hatası", "Liste güncellendi ancak kaydedilemedi.", { isError: true });
                });
                return true; // Updated

            } else {
                logger.log("Liste zaten güncel.");
                config.lastUpdate = Date.now();
                await GM_setValue(KEY_LAST_UPDATE, config.lastUpdate).catch(e=>logger.error("Zaman damgası kaydı başarısız:", e));
                return false; // Not updated
            }
        } catch (err) {
            logger.error("Liste İŞLEME hatası:", err);
            showFeedback("Liste İşleme Hatası", `Liste işlenemedi.\n${err.message}`, { isError: true });
            return false;
        }
    };

    function applyFilterAction(entry, author) {
        const entryId = entry.dataset.id || 'ID Yok';
        if (config.filterMode === "hide") {
            entry.classList.add(`${CSS_PREFIX}hidden`);
            logger.debug(`Gizlendi: ${entryId} (${author})`);
            return 'hide';
        }

        // Collapse Mode
        const contentEl = entry.querySelector(".content");
        if (!contentEl) return 'collapse_failed_no_content';
        if (entry.classList.contains(`${CSS_PREFIX}collapsed`)) return 'already_collapsed';

        const originalDisplay = contentEl.style.display;
        contentEl.style.display = 'none';

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

            const footerEl = entry.querySelector('footer');
            entry.insertBefore(placeholder, footerEl || entry.lastElementChild); // Insert before footer or as last element

            const showLink = placeholder.querySelector(`.${CSS_PREFIX}show-link a`);
            if (showLink) {
                showLink.addEventListener("click", (e) => {
                    e.preventDefault(); e.stopPropagation();
                    contentEl.style.display = originalDisplay || '';
                    placeholder.remove();
                    entry.classList.remove(`${CSS_PREFIX}collapsed`);
                    const footer = entry.querySelector('footer');
                    if (footer && !footer.querySelector(`.${CSS_PREFIX}opened-warning`)) {
                        const warningSpan = document.createElement('span');
                        warningSpan.className = `${CSS_PREFIX}opened-warning`;
                        warningSpan.textContent = '⚠️ Filtrelendi';
                        warningSpan.title = `'${author}' yazarına ait bu içerik filtre nedeniyle daraltılmıştı.`;
                        footer.appendChild(warningSpan);
                    }
                }, { once: true });
            }
        } else {
             placeholder.style.display = 'flex';
        }

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

    function enhanceEntry(entry, isFirstOnPage = false) {
        if (entry.dataset.efhProcessed === 'true') return;
        const author = entry.dataset.author?.toLowerCase().trim();
        if (!author) { entry.dataset.efhProcessed = 'true'; return; } // Skip if no author

        const entryId = entry.dataset.id || 'ID Yok';
        let action = 'none';
        try {
            if (!config.paused && filteredAuthorsSet.has(author)) {
                if (isFirstOnPage) firstEntryAuthorFiltered = true;
                filteredEntryCount++;
                action = applyFilterAction(entry, entry.dataset.author); // Use original case for display

                // Update total count (fire and forget)
                config.totalFiltered = (config.totalFiltered || 0) + 1;
                GM_setValue(KEY_TOTAL_FILTERED, config.totalFiltered).catch(err => logger.warn("Toplam sayaç kaydedilemedi:", err));
            } else {
                action = config.paused ? 'paused' : 'not_in_list';
            }
        } catch (err) {
            logger.error(`Entry ${entryId} İŞLENEMEDİ (Yazar: ${author}):`, err);
            action = 'error';
        }
        entry.dataset.efhProcessed = 'true';
        entry.dataset.efhAction = action;
    }

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

            if (!config.showWarning || config.paused) return;

            const titleH1 = document.getElementById("title");
            if (!titleH1) return;
            const targetElement = titleH1.querySelector('a') || titleH1;

            const showWarning = firstEntryAuthorFiltered || filteredEntryCount > 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]";

                let title = "Filtre uygulandı: ";
                title += firstEntryAuthorFiltered ? "İlk entry yazarı" : "";
                title += (firstEntryAuthorFiltered && filteredEntryCount > 1) ? " ve " : "";
                title += (!firstEntryAuthorFiltered && filteredEntryCount > TOPIC_WARNING_THRESHOLD) ? `${filteredEntryCount} yazar` : "";
                title += (firstEntryAuthorFiltered && filteredEntryCount > 1 && filteredEntryCount <= TOPIC_WARNING_THRESHOLD + 1) ? `${filteredEntryCount - 1} diğer yazar` : "";
                 title += " filtrelendi.";
                 topicWarningElement.title = title.replace(" ve  filtrelendi.", " filtrelendi."); // Clean up edge case

                targetElement.appendChild(topicWarningElement);
                logger.debug("Konu başlığı uyarısı eklendi.");
            }
        } catch (err) {
            logger.error("Konu başlığı uyarısı HATA:", err);
            topicWarningElement?.remove(); topicWarningElement = null;
        }
    };

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

        const selector = '#entry-item-list > li[data-author]:not([data-efh-processed="true"])';
        const entries = document.querySelectorAll(selector);

        if (entries.length > 0) {
            logger.log(`${entries.length} yeni entry işlenecek.`);
            const isFirstEntry = (el) => el === entries[0] && el.parentElement?.firstChild === el;
            entries.forEach(entry => enhanceEntry(entry, isFirstEntry(entry)));
            updateTopicWarning(); // Update warning after processing the batch
            logger.log(`Bu parti işlem tamam. Filtrelenen: ${filteredEntryCount}`);
        }
    }, DEBOUNCE_DELAY_MS);

    // --- Init ---
    async function initialize() {
        logger.log("Script BAŞLATILIYOR...");
        await loadConfig();

        if (!config.paused) {
            const now = Date.now();
            const timeSinceUpdate = now - (config.lastUpdate || 0);
            if (filteredListSize === 0 || timeSinceUpdate > UPDATE_INTERVAL_MS) {
                logger.log(`Liste ${filteredListSize === 0 ? 'boş' : 'güncel değil'}. Arka planda senkronizasyon deneniyor...`);
                syncList(filteredListSize === 0).then(updated => {
                    if (updated) {
                        logger.log("Arka plan liste güncellemesi TAMAM. Yeni boyut: " + filteredListSize);
                        processVisibleEntries(); // Re-process if list changed after initial load
                    }
                }).catch(err => logger.error("Arka plan sync hatası:", err));
            } else {
                logger.log("Liste güncel görünüyor.");
            }
            if (filteredAuthorsSet.size === 0) logger.warn("UYARI: Filtre listesi boş!");
        } else {
            logger.log("Filtre DURDURULMUŞ.");
        }

        processVisibleEntries(); // Initial processing

        // Observer Setup
        const entryListContainer = document.querySelector('#entry-item-list');
        if (entryListContainer) {
            try {
                const observer = new MutationObserver(mutations => {
                    let needsProcessing = 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]:not([data-efh-processed="true"])') || n.querySelector?.('li[data-author]:not([data-efh-processed="true"])')))
                    );
                    if (needsProcessing) {
                        logger.debug("Observer: Yeni entry(ler) tespit edildi.");
                        processVisibleEntries();
                    }
                });
                observer.observe(entryListContainer, { childList: true, subtree: true });
                logger.log("#entry-item-list İZLENİYOR.");
            } catch (err) {
                logger.error("MutationObserver BAŞLATILAMADI:", err);
                showFeedback("Kritik Hata", "Sayfa değişiklikleri İZLENEMİYOR!", { isError: true });
            }
        } else {
            logger.warn("#entry-item-list BULUNAMADI! Dinamik entry'ler işlenemeyecek.");
        }

        registerMenuCommands();
        logger.log(`🎉 ${SCRIPT_NAME} yüklendi. v${GM_info?.script?.version || '?'}`);
    }

    // --- Menu ---
    function registerMenuCommands() {
        const setConfigAndReload = async (key, value, msg) => {
            try {
                await GM_setValue(key, value);
                showFeedback("Ayar Değiştirildi", msg, { silent: true });
                location.reload();
            } catch (err) {
                showFeedback("Depolama Hatası", `Ayar (${key}) kaydedilemedi.\n${err.message}`, { isError: true });
                registerMenuCommands(); // Re-register to show old state
            }
        };

        GM_registerMenuCommand(`${config.paused ? "▶️ Filtreyi AKTİF ET" : "⏸️ Filtreyi DURDUR"}`, () => {
            setConfigAndReload(KEY_PAUSED, !config.paused, `Filtre ${!config.paused ? 'AKTİF' : 'DURDU'}. Sayfa yenileniyor...`, true);
        });
        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'}" oldu. Sayfa yenileniyor...`, true);
        });
        GM_registerMenuCommand(`Başlık Uyarısını ${config.showWarning ? "🚫 Gizle" : "⚠️ Göster"}`, () => {
            setConfigAndReload(KEY_SHOW_WARNING, !config.showWarning, `Başlık uyarısı ${!config.showWarning ? 'gösterilecek' : 'gizlenecek'}. Sayfa yenileniyor...`, true);
        });
        GM_registerMenuCommand("🔄 Listeyi ŞİMDİ Güncelle", async () => {
            showFeedback("Güncelleme", "Liste çekiliyor...", { silent: true });
            const updated = await syncList(true);
            if (updated) {
                showFeedback("Başarılı", `Liste güncellendi (${filteredListSize} yazar). Sayfa yenileniyor...`);
                location.reload();
            } else {
                 showFeedback("Güncelleme", "Liste güncellenemedi veya zaten günceldi.", { silent: false });
            }
        });
        GM_registerMenuCommand(`📊 Filtre İstatistikleri`, async () => {
            const total = await GM_getValue(KEY_TOTAL_FILTERED, config.totalFiltered); // Re-read
            const lastUpdate = config.lastUpdate ? new Date(config.lastUpdate).toLocaleString("tr-TR") : "Hiç";
            showFeedback("İstatistik", `Toplam Filtrelenen: ${total}\nMevcut Liste Boyutu: ${filteredListSize}\nSon Güncelleme: ${lastUpdate}`);
        });
        GM_registerMenuCommand(`🗑️ Önbelleği Temizle`, async () => {
            if (confirm(`[${SCRIPT_NAME}] Emin misiniz?\n\nYerel liste önbelleği ve zaman damgası silinecek.`)) {
                 try {
                     await Promise.all([GM_deleteValue(KEY_LIST_RAW), GM_deleteValue(KEY_LAST_UPDATE)]);
                     filteredAuthorsSet = new Set(); filteredListSize = 0; config.listRaw = ""; config.lastUpdate = 0;
                     showFeedback("Başarılı", "Önbellek temizlendi. Sayfa yenilenince liste yeniden yüklenecek.");
                     location.reload();
                 } catch (err) { showFeedback("Hata", `Önbellek temizlenemedi: ${err.message}`, { isError: true }); }
            } else { showFeedback("İptal", "Önbellek silinmedi.", { silent: true }); }
        });
    }

    // --- Start ---
    initialize().catch(err => {
        logger.error("Başlatılırken YAKALANAMAYAN KRİTİK HATA:", err);
        alert(`[${SCRIPT_NAME}] BAŞLATMA BAŞARISIZ!\n\nHata: ${err.message}`);
    });

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