Booru → Eagle cool Saver

Save imgs from booru directly to Eagle cool (single-worker queue, draggable panel)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Booru → Eagle cool Saver
// @namespace    booru-eagle
// @version      1.5 Final
// @license      MIT
// @description  Save imgs from booru directly to Eagle cool (single-worker queue, draggable panel)
// @match        https://rule34.xxx/*
// @match        https://danbooru.donmai.us/*
// @match        https://chan.sankakucomplex.com/*
// @match        https://gelbooru.com/*
// @match        https://konachan.com/*
// @match        https://eagle.cool/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addValueChangeListener
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function () {
    "use strict";

    const KEYS = {
        API_TOKEN: "eagle_api_token",
        API_PORT: "eagle_api_port",
        SETTINGS_DISMISSED: "eagle_settings_dismissed",
        PARENT_ID: "eagle_parent_id",
        QUEUE: "eagle_queue_v5",
        WORKER: "eagle_worker_v5",
        PANEL_STATE: "eagle_panel_v3",
        AUTO_CLOSE: "eagle_autoclose_v3",
        UI_STATE: "eagle_ui_state_v2",
        PROGRESS: "eagle_progress_v2",
    };

    const CONFIG = {
        DEFAULT_PORT: "41595",
        DEFAULT_TOKEN: "Your Eagle API token here",
        WORKER_TIMEOUT: 25000,
        WORKER_INTERVAL: 1500,
        HEARTBEAT_INTERVAL: 5000,
        WORKER_ELECTION_MIN_DELAY: 200,
        WORKER_ELECTION_MAX_DELAY: 800,
        WORKER_RECOVERY_INTERVAL: 5000,
        ENQUEUE_MAX_RETRIES: 5,
        ENQUEUE_BASE_DELAY: 50,
        ENQUEUE_JITTER: 50,
        DEBOUNCE_DELAY: 150,
        DEBOUNCE_RESET_DELAY: 100,
        PROGRESS_INCREMENT: 5,
        PROGRESS_INCREMENT_INTERVAL: 50,
        PROGRESS_MAX_SHOW: 90,
        PROGRESS_HIDE_DELAY: 400,
        AUTOCLOSE_FALLBACK_TIMEOUT: 300000,
        AUTOCLOSE_CLOSE_DELAY: 500,
        PANEL_CHECK_INTERVAL: 1000,
        TOAST_DISPLAY_TIME: 2500,
        TOAST_FADE_DURATION: 200,
        SYNC_APPLY_DELAY: 50,
        QUEUE_PROCESS_DELAY: 500,
    };

    const TAB_ID = generateTabId();

    const DOM = {
        panel: null,
        header: null,
        body: null,
        saveBtn: null,
        parentBtn: null,
        stopBtn: null,
        autoCloseBtn: null,
        resInfo: null,
        queueInfo: null,
        workerBadge: null,
        collapseBtn: null,
        progressOuter: null,
        progressInner: null,
    };

    const worker = {
        isWorker: false,
        timer: null,
        heartbeatTimer: null,
        processing: false,
        currentPostId: null,
    };

    const ui = {
        collapsed: false,
        autoCloseEnabled: false,
        panelSyncing: false,
        progressSyncing: false,
        uiStateSyncing: false,
        _applyingQueueUpdate: false,
        _applyingWorkerUpdate: false,
        _applyingUIState: false,
        _applyingProgress: false,
        _applyingPanelState: false,
        _closingTab: false,
    };

    const debounceTimers = {
        uiState: null,
        panelState: null,
        progress: null,
    };

    const autoClose = {
        pendingPostId: null,
        timer: null,
        FALLBACK_TIMEOUT: CONFIG.AUTOCLOSE_FALLBACK_TIMEOUT,
    };

    const progress = {
        interval: null,
    };

    function generateTabId() {
        return Date.now().toString(36) + Math.random().toString(36).substr(2, 6);
    }

    function log(level, ...args) {
        const prefix = `[Eagle ${TAB_ID}]`;
        if (level === 'error') console.error(prefix, ...args);
        else if (level === 'warn') console.warn(prefix, ...args);
        else console.log(prefix, ...args);
    }

    function getQueue() {
        try {
            const raw = GM_getValue(KEYS.QUEUE, null);
            if (!raw) return [];
            const parsed = JSON.parse(raw);
            return Array.isArray(parsed) ? parsed : [];
        } catch (e) {
            log('error', 'Queue parse error:', e);
            return [];
        }
    }

    function saveQueue(queue) {
        try {
            GM_setValue(KEYS.QUEUE, JSON.stringify(queue));
        } catch (e) {
            log('error', 'Queue save error:', e);
        }
    }

    async function enqueueItem(url, tags, postId) {
        if (!postId || !url) {
            log('warn', 'enqueueItem: missing postId or url');
            return false;
        }

        const maxRetries = CONFIG.ENQUEUE_MAX_RETRIES;
        const baseDelay = CONFIG.ENQUEUE_BASE_DELAY;

        for (let attempt = 1; attempt <= maxRetries; attempt++) {
            const queue = getQueue();

            if (queue.some(item => item.postId === postId)) {
                log('warn', `Post ${postId} already in queue`);
                toast("⚠ Already in queue");
                return false;
            }

            queue.push({
                postId,
                url,
                tags,
                timestamp: Date.now(),
                addedByTab: TAB_ID,
            });

            saveQueue(queue);

            const verify = getQueue();
            if (verify.some(item => item.postId === postId)) {
                log('info', `Enqueued ${postId}, queue length: ${verify.length}`);
                return true;
            }

            const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * CONFIG.ENQUEUE_JITTER;
            log('warn', `Queue conflict on ${postId}, attempt ${attempt}/${maxRetries}, retrying in ${Math.round(delay)}ms`);

            await new Promise(resolve => setTimeout(resolve, delay));
        }

        log('error', `Failed to enqueue ${postId} after ${maxRetries} attempts`);
        toast("⚠ Failed to add to queue — too many conflicts");
        return false;
    }

    function dequeueItem(postId) {
        const queue = getQueue();
        const idx = queue.findIndex(item => item.postId === postId);

        if (idx === -1) return false;

        queue.splice(idx, 1);
        saveQueue(queue);

        const verify = getQueue();
        if (!verify.some(item => item.postId === postId)) {
            log('info', `Dequeued ${postId}, queue length: ${verify.length}`);
            return true;
        }

        log('warn', `Dequeue verification failed for ${postId}`);
        return false;
    }

    function clearQueue() {
        const queue = getQueue();
        const wasProcessing = worker.processing;
        const currentPostId = worker.currentPostId;

        saveQueue([]);

        if (worker.processing) {
            worker.processing = false;
            worker.currentPostId = null;
            log('warn', `Cancelled processing of ${currentPostId}`);
        }

        showProgressBar(false);

        updateQueueUI();

        const count = queue.length;
        if (count > 0 || wasProcessing) {
            const text = wasProcessing
                ? `Cleared ${count} items + cancelled ${currentPostId}`
                : `Cleared ${count} items from queue`;
            log('info', text);
            toast(`✓ ${text}`);
        } else {
            toast("Queue already empty");
        }
    }

    function getActiveWorker() {
        try {
            const w = GM_getValue(KEYS.WORKER, null);
            if (!w) return null;

            const age = Date.now() - w.heartbeat;
            if (age > CONFIG.WORKER_TIMEOUT) {
                log('warn', `Worker ${w.tabId} expired (${age}ms ago)`);
                return null;
            }

            return w;
        } catch (e) {
            log('error', 'getActiveWorker error:', e);
            return null;
        }
    }

    function becomeWorker() {
        const active = getActiveWorker();

        if (active && active.tabId !== TAB_ID) {
            log('info', `Cannot become worker — ${active.tabId} is active`);
            return false;
        }

        worker.isWorker = true;
        saveWorkerState();
        startHeartbeat();

        log('info', 'Became WORKER');
        updateWorkerIndicator();
        return true;
    }

    function resignWorker() {
        worker.isWorker = false;
        worker.processing = false;
        worker.currentPostId = null;

        stopHeartbeat();
        stopWorkerLoop();

        const active = getActiveWorker();
        if (active && active.tabId === TAB_ID) {
            GM_setValue(KEYS.WORKER, null);
        }

        log('info', 'Resigned as worker');
        updateWorkerIndicator();
    }

    function saveWorkerState() {
        GM_setValue(KEYS.WORKER, {
            tabId: TAB_ID,
            heartbeat: Date.now(),
        });
    }

    function heartbeat() {
        const active = getActiveWorker();

        if (active && active.tabId === TAB_ID) {
            saveWorkerState();
        } else {
            if (worker.isWorker) {
                log('warn', 'Lost worker status, resigning');
                resignWorker();
            }
        }
    }

    function startHeartbeat() {
        stopHeartbeat();
        worker.heartbeatTimer = setInterval(heartbeat, CONFIG.HEARTBEAT_INTERVAL);
    }

    function stopHeartbeat() {
        if (worker.heartbeatTimer) {
            clearInterval(worker.heartbeatTimer);
            worker.heartbeatTimer = null;
        }
    }

    function startWorkerLoop() {
        stopWorkerLoop();

        worker.timer = setInterval(() => {
            if (!worker.isWorker) {
                stopWorkerLoop();
                return;
            }

            const queue = getQueue();
            if (queue.length === 0) {
                resignWorker();
                updateQueueUI();
                return;
            }

            heartbeat();
            processNextItem();
        }, CONFIG.WORKER_INTERVAL);

        setTimeout(() => {
            if (worker.isWorker) {
                heartbeat();
                processNextItem();
            }
        }, CONFIG.WORKER_ELECTION_MIN_DELAY);
    }

    function stopWorkerLoop() {
        if (worker.timer) {
            clearInterval(worker.timer);
            worker.timer = null;
        }
    }

    function tryBecomeWorker() {
        if (worker.isWorker) return;

        const active = getActiveWorker();
        if (!active) {
            const randomDelay = CONFIG.WORKER_ELECTION_MIN_DELAY + Math.random() * (CONFIG.WORKER_ELECTION_MAX_DELAY - CONFIG.WORKER_ELECTION_MIN_DELAY);
            log('info', `Attempting to become worker in ${Math.round(randomDelay)}ms`);

            setTimeout(() => {
                const stillNoActive = getActiveWorker();
                if (!stillNoActive && !worker.isWorker) {
                    if (becomeWorker()) {
                        startWorkerLoop();
                    }
                }
            }, randomDelay);
        }
    }

    async function processNextItem() {
        if (!worker.isWorker || worker.processing) return;

        const queue = getQueue();
        if (queue.length === 0) {
            updateQueueUI();
            return;
        }

        const item = queue[0];

        worker.processing = true;
        worker.currentPostId = item.postId;

        log('info', `Processing ${item.postId} (${item.url.substring(0, 50)}...)`);
        updateQueueUI();
        saveProgressState(true, 0);

        try {
            const success = await sendToEagle(item.url, item.tags);

            if (success) {
                dequeueItem(item.postId);
                saveProgressState(false, 100);
            } else {
                log('warn', `Failed to send ${item.postId}, keeping in queue`);
                saveProgressState(false, 0);
            }
        } catch (err) {
            log('error', `Error processing ${item.postId}:`, err);
            saveProgressState(false, 0);
        } finally {
            worker.processing = false;
            worker.currentPostId = null;
            updateQueueUI();
            checkAutoClose();
        }
    }

    function waitForAutoClose(postId) {
        if (autoClose.timer) {
            clearTimeout(autoClose.timer);
            autoClose.timer = null;
        }

        autoClose.pendingPostId = postId;

        toast("✓ Queued — will close after upload");

        autoClose.timer = setTimeout(() => {
            if (autoClose.pendingPostId === postId) {
                log('warn', `Auto-close fallback for ${postId}, closing tab`);
                autoClose.pendingPostId = null;
                autoClose.timer = null;
                closeTab();
            }
        }, autoClose.FALLBACK_TIMEOUT);
    }

    function checkAutoClose() {
        if (!autoClose.pendingPostId) return;

        const queue = getQueue();
        const stillInQueue = queue.some(item => item.postId === autoClose.pendingPostId);
        const stillProcessing = worker.currentPostId === autoClose.pendingPostId;

        if (!stillInQueue && !stillProcessing) {
            log('info', `Auto-close: ${autoClose.pendingPostId} processed, closing tab`);
            toast("✓ Done — closing tab");

            if (autoClose.timer) {
                clearTimeout(autoClose.timer);
                autoClose.timer = null;
            }

            autoClose.pendingPostId = null;

            setTimeout(() => closeTab(), CONFIG.AUTOCLOSE_CLOSE_DELAY);
        }
    }

    function closeTab() {
        if (ui._closingTab) {
            log('warn', 'closeTab called while already closing — ignoring');
            return;
        }
        ui._closingTab = true;

        autoClose.pendingPostId = null;
        if (autoClose.timer) {
            clearTimeout(autoClose.timer);
            autoClose.timer = null;
        }

        log('info', `Attempting to close tab (history.length=${history.length})`);

        if (history.length > 1) {
            log('info', 'Navigating back (history.back)');
            history.back();
        } else {
            try {
                const closed = window.close();
                if (closed || window.closed) {
                    log('info', 'Tab closed via window.close()');
                    return;
                }
            } catch (e) {
                log('warn', 'window.close() threw:', e);
            }
            try {
                location.replace('about:blank');
            } catch (e2) {
                log('error', 'All close methods failed');
            }
        }

        setTimeout(() => {
            ui._closingTab = false;
        }, 2000);
    }

    function saveUIState() {
        if (!DOM.panel || ui._applyingUIState) return;

        if (debounceTimers.uiState) {
            clearTimeout(debounceTimers.uiState);
        }

        debounceTimers.uiState = setTimeout(() => {
            const state = {
                autoClose: ui.autoCloseEnabled,
                parentID: GM_getValue(KEYS.PARENT_ID, null),
                tabId: TAB_ID,
                timestamp: Date.now(),
            };

            ui.uiStateSyncing = true;
            GM_setValue(KEYS.UI_STATE, JSON.stringify(state));
            setTimeout(() => { ui.uiStateSyncing = false; }, CONFIG.DEBOUNCE_RESET_DELAY);
            debounceTimers.uiState = null;
        }, CONFIG.DEBOUNCE_DELAY);
    }

    function applyUIState(state) {
        if (!state || !DOM.panel || ui._applyingUIState) return;

        ui._applyingUIState = true;

        try {
            if (state.autoClose !== undefined && state.autoClose !== ui.autoCloseEnabled) {
                ui.autoCloseEnabled = state.autoClose;
                updateAutoCloseBtn();
                if (DOM.saveBtn) {
                    DOM.saveBtn.textContent = ui.autoCloseEnabled ? "Save+Close" : "Save";
                    DOM.saveBtn.title = ui.autoCloseEnabled
                        ? "Alt+Z — add to queue & close tab"
                        : "Alt+Z — add to queue";
                }
            }

            updatePostPageUI();

            if (DOM.resInfo) {
                const onPost = isPostPage();
                const resolution = getImageResolution();
                DOM.resInfo.textContent = onPost ? resolution : "[not a post page]";
            }

            if (state.parentID !== undefined) {
                updateColors();
            }
        } finally {
            setTimeout(() => { ui._applyingUIState = false; }, CONFIG.SYNC_APPLY_DELAY);
        }
    }

    function saveProgressState(show, width) {
        if (ui._applyingProgress) return;

        if (debounceTimers.progress) {
            clearTimeout(debounceTimers.progress);
        }

        debounceTimers.progress = setTimeout(() => {
            ui.progressSyncing = true;
            GM_setValue(KEYS.PROGRESS, JSON.stringify({
                show,
                width,
                tabId: TAB_ID,
                timestamp: Date.now()
            }));
            setTimeout(() => { ui.progressSyncing = false; }, CONFIG.DEBOUNCE_RESET_DELAY);
            debounceTimers.progress = null;
        }, CONFIG.DEBOUNCE_DELAY);
    }

    function applyProgressState(state) {
        if (!state || !DOM.progressOuter || !DOM.progressInner || ui._applyingProgress) return;

        ui._applyingProgress = true;

        try {
            if (state.show) {
                DOM.progressOuter.style.display = "block";
                DOM.progressInner.style.width = (state.width || 0) + "%";
            } else {
                DOM.progressOuter.style.display = "none";
                DOM.progressInner.style.width = "0%";
            }
        } finally {
            setTimeout(() => { ui._applyingProgress = false; }, CONFIG.SYNC_APPLY_DELAY);
        }
    }

    function savePanelState() {
        if (!DOM.panel || ui._applyingPanelState) return;

        if (debounceTimers.panelState) {
            clearTimeout(debounceTimers.panelState);
        }

        debounceTimers.panelState = setTimeout(() => {
            const state = {
                left: DOM.panel.style.left,
                top: DOM.panel.style.top,
                transform: DOM.panel.style.transform,
                collapsed: ui.collapsed,
            };

            ui.panelSyncing = true;
            GM_setValue(KEYS.PANEL_STATE, JSON.stringify(state));
            setTimeout(() => { ui.panelSyncing = false; }, CONFIG.DEBOUNCE_RESET_DELAY);
            debounceTimers.panelState = null;
        }, CONFIG.DEBOUNCE_DELAY);
    }

    function applyPanelState(state) {
        if (!state || !DOM.panel || ui._applyingPanelState) return;

        ui._applyingPanelState = true;

        try {
            DOM.panel.style.left = state.left;
            DOM.panel.style.top = state.top;
            DOM.panel.style.transform = state.transform;

            if (state.collapsed !== undefined && state.collapsed !== ui.collapsed) {
                setCollapsed(state.collapsed);
            }
        } finally {
            setTimeout(() => { ui._applyingPanelState = false; }, CONFIG.SYNC_APPLY_DELAY);
        }
    }

    function setupValueChangeListeners() {
        GM_addValueChangeListener(KEYS.QUEUE, (name, oldVal, newVal) => {
            if (ui._applyingQueueUpdate) return;

            ui._applyingQueueUpdate = true;
            try {
                updateQueueUI();
                checkAutoClose();

                if (worker.isWorker && !worker.processing) {
                    setTimeout(() => processNextItem(), CONFIG.QUEUE_PROCESS_DELAY);
                }
            } finally {
                setTimeout(() => { ui._applyingQueueUpdate = false; }, CONFIG.DEBOUNCE_RESET_DELAY);
            }
        });

        GM_addValueChangeListener(KEYS.WORKER, (name, oldVal, newVal) => {
            if (ui._applyingWorkerUpdate) return;

            ui._applyingWorkerUpdate = true;
            try {
                if (!worker.isWorker) {
                    const randomDelay = 1000 + Math.random() * 1000;
                    setTimeout(() => {
                        const active = getActiveWorker();
                        if (!active && getQueue().length > 0) {
                            tryBecomeWorker();
                        }
                    }, randomDelay);
                }
            } finally {
                setTimeout(() => { ui._applyingWorkerUpdate = false; }, CONFIG.DEBOUNCE_RESET_DELAY);
            }
        });

        GM_addValueChangeListener(KEYS.UI_STATE, (name, oldVal, newVal) => {
            if (ui.uiStateSyncing || ui._applyingUIState) return;
            try {
                if (newVal) {
                    ui._applyingUIState = true;
                    applyUIState(JSON.parse(newVal));
                }
            } catch (e) {
                log('warn', 'UI_STATE parse error:', e);
            }
        });

        GM_addValueChangeListener(KEYS.PROGRESS, (name, oldVal, newVal) => {
            if (ui.progressSyncing || ui._applyingProgress) return;
            try {
                if (newVal) {
                    ui._applyingProgress = true;
                    applyProgressState(JSON.parse(newVal));
                }
            } catch (e) {
                log('warn', 'PROGRESS parse error:', e);
            }
        });

        GM_addValueChangeListener(KEYS.PANEL_STATE, (name, oldVal, newVal) => {
            if (ui.panelSyncing || ui._applyingPanelState) return;
            try {
                if (newVal) {
                    ui._applyingPanelState = true;
                    applyPanelState(JSON.parse(newVal));
                }
            } catch (e) {
                log('warn', 'PANEL_STATE parse error:', e);
            }
        });
    }

    function startWorkerRecoveryCheck() {
        setInterval(() => {
            if (!worker.isWorker) {
                const active = getActiveWorker();
                const queue = getQueue();

                if (!active && queue.length > 0) {
                    log('info', 'No active worker found in recovery check, trying to become worker');
                    tryBecomeWorker();
                }
            }
        }, CONFIG.WORKER_RECOVERY_INTERVAL);
    }

    function getApiUrl() {
        const port = GM_getValue(KEYS.API_PORT, CONFIG.DEFAULT_PORT);
        const token = GM_getValue(KEYS.API_TOKEN, null);

        if (!token) {
            log('warn', 'API token not configured');
            return null;
        }

        return `http://localhost:${port}/api/item/addFromURL?token=${token}`;
    }

    async function sendToEagle(url, tags) {
        if (!url) {
            log('warn', 'sendToEagle: empty URL');
            toast("✗ No URL");
            return false;
        }

        const apiUrl = getApiUrl();
        if (!apiUrl) {
            log('error', 'sendToEagle: API not configured');
            toast("⚠️ No API token! (Alt+S to configure)");
            return false;
        }

        showProgressBar(true);

        let finalUrl = url;
        if (isSankaku() && !url.startsWith('data:')) {
            try {
                finalUrl = await toDataURL(url);
            } catch (err) {
                log('error', 'Sankaku URL conversion failed:', err);
                toast("⚠ Image conversion failed, using original URL");
            }
        }

        const payload = JSON.stringify({
            url: finalUrl,
            name: document.title,
            website: location.href,
            tags: tags,
            headers: { referer: location.href }
        });

        log('info', `Sending to Eagle: ${finalUrl.substring(0, 60)}...`);

        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: apiUrl,
                headers: { "Content-Type": "application/json" },
                data: payload,
                timeout: 60000,
                onload: function(response) {
                    showProgressBar(false);

                    try {
                        const data = JSON.parse(response.responseText);

                        if (data.status === "success") {
                            log('info', 'Successfully saved to Eagle');
                            toast("✓ Saved to Eagle");
                            resolve(true);
                        } else {
                            log('error', 'Eagle API error:', data);
                            toast("✗ Eagle: " + (data.message || "Unknown error"));
                            resolve(false);
                        }
                    } catch (e) {
                        log('error', 'Response parse error:', e,
                            'Response:', response.responseText?.substring(0, 200));
                        toast("✗ Response parse error");
                        resolve(false);
                    }
                },
                onerror: function(err) {
                    showProgressBar(false);
                    log('error', 'Network error:', err);
                    toast("✗ Network error — is Eagle running?");
                    resolve(false);
                },
                ontimeout: function() {
                    showProgressBar(false);
                    log('warn', 'Request timeout (60s)');
                    toast("✗ Timeout — Eagle may be busy");
                    resolve(false);
                }
            });
        });
    }

    async function toDataURL(url) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                responseType: "blob",
                timeout: 60000,
                onload: function(response) {
                    const blob = response.response;

                    if (!blob) {
                        log('error', 'toDataURL: empty response');
                        resolve(url);
                        return;
                    }

                    const reader = new FileReader();
                    reader.onloadend = () => resolve(reader.result);
                    reader.onerror = () => {
                        log('error', 'toDataURL: FileReader error');
                        resolve(url);
                    };
                    reader.readAsDataURL(blob);
                },
                onerror: function(err) {
                    log('error', 'toDataURL: network error', err);
                    resolve(url);
                },
                ontimeout: function() {
                    log('warn', 'toDataURL: timeout');
                    resolve(url);
                }
            });
        });
    }

    function toast(text) {
        if (!document.body) return;

        const d = document.createElement("div");
        d.textContent = text;
        d.style.cssText = `
            position:fixed;top:20px;right:20px;
            background:rgba(18,18,24,0.92);
            backdrop-filter:blur(12px);
            color:#e8e8e8;
            padding:9px 16px;
            border-radius:10px;
            z-index:1000000;
            font-size:13px;
            font-family:'Segoe UI',system-ui,sans-serif;
            font-weight:500;
            box-shadow:0 6px 20px rgba(0,0,0,0.4);
            border:1px solid rgba(255,255,255,0.06);
            opacity:0;
            transform:translateY(-8px);
            transition:opacity 0.2s ease,transform 0.2s ease;
        `;
        document.body.appendChild(d);

        requestAnimationFrame(() => {
            d.style.opacity = "1";
            d.style.transform = "translateY(0)";
        });

        setTimeout(() => {
            d.style.opacity = "0";
            d.style.transform = "translateY(-8px)";
            setTimeout(() => d.remove(), CONFIG.TOAST_FADE_DURATION);
        }, CONFIG.TOAST_DISPLAY_TIME);
    }

    function createPanel() {
        if (DOM.panel) return;

        DOM.panel = document.createElement("div");
        DOM.panel.id = "eagle-panel";
        DOM.panel.style.cssText = `
            position: fixed;
            top: 50px;
            left: 50%;
            transform: translateX(-50%);
            min-width: 280px;
            z-index: 999999;
            font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
            background: rgba(18, 18, 24, 0.88);
            backdrop-filter: blur(16px) saturate(180%);
            -webkit-backdrop-filter: blur(16px) saturate(180%);
            border: 1px solid rgba(255, 255, 255, 0.08);
            border-radius: 14px;
            box-shadow: 0 12px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04) inset;
            overflow: hidden;
            transition: box-shadow 0.3s ease;
            user-select: none;
        `;

        DOM.header = document.createElement("div");
        DOM.header.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 10px 14px;
            cursor: grab;
            border-bottom: 1px solid rgba(255,255,255,0.06);
            background: rgba(255,255,255,0.02);
        `;
        DOM.header.onmouseenter = () => {
            DOM.panel.style.boxShadow = "0 16px 48px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06) inset";
        };
        DOM.header.onmouseleave = () => {
            DOM.panel.style.boxShadow = "0 12px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04) inset";
        };

        const title = document.createElement("span");
        title.style.cssText = "color: #e8e8e8; font-size: 13px; font-weight: 600; letter-spacing: 0.3px;";
        title.textContent = "Eagle Saver";

        DOM.workerBadge = document.createElement("span");
        DOM.workerBadge.style.cssText = "font-size: 12px; line-height: 1; transition: all 0.3s ease;";

        DOM.collapseBtn = document.createElement("button");
        DOM.collapseBtn.style.cssText = `
            background: none;
            border: none;
            color: #888;
            cursor: pointer;
            font-size: 16px;
            padding: 2px 6px;
            border-radius: 6px;
            line-height: 1;
            transition: all 0.2s ease;
            transform: rotate(180deg);
        `;
        DOM.collapseBtn.textContent = "▾";
        DOM.collapseBtn.onmouseenter = () => {
            DOM.collapseBtn.style.color = "#fff";
            DOM.collapseBtn.style.background = "rgba(255,255,255,0.08)";
        };
        DOM.collapseBtn.onmouseleave = () => {
            DOM.collapseBtn.style.color = "#888";
            DOM.collapseBtn.style.background = "none";
        };
        DOM.collapseBtn.onclick = (e) => {
            e.stopPropagation();
            toggleCollapse();
        };

        DOM.header.appendChild(title);
        DOM.header.appendChild(DOM.workerBadge);
        DOM.header.appendChild(DOM.collapseBtn);
        DOM.panel.appendChild(DOM.header);

        DOM.body = document.createElement("div");
        DOM.body.style.cssText = `
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 6px;
            padding: 10px 14px 12px;
            max-height: 500px;
            opacity: 1;
            pointer-events: auto;
            transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
        `;

        const btnBox = document.createElement("div");
        btnBox.style.cssText = "display:flex;gap:6px;flex-wrap:wrap;justify-content:center;";

        DOM.saveBtn = document.createElement("button");
        DOM.saveBtn.textContent = "Save";
        DOM.saveBtn.title = "Alt+Z — add to queue";

        DOM.parentBtn = document.createElement("button");
        DOM.parentBtn.textContent = "Parent";
        DOM.parentBtn.title = "Alt+X — set parent ID & save";

        DOM.stopBtn = document.createElement("button");
        DOM.stopBtn.textContent = "Stop";
        DOM.stopBtn.title = "Alt+C — clear parent, stop worker & clear queue";

        DOM.autoCloseBtn = document.createElement("button");
        DOM.autoCloseBtn.textContent = "⏻";
        DOM.autoCloseBtn.title = "Auto-close tab after Save";

        const baseBtnStyle = "padding:7px 14px;border:none;border-radius:8px;cursor:pointer;font-size:12px;font-weight:600;letter-spacing:0.2px;transition:all 0.15s ease;";

        DOM.saveBtn.style.cssText = baseBtnStyle + "background:rgba(255,152,0,0.85);color:#fff;";
        DOM.parentBtn.style.cssText = baseBtnStyle + "background:rgba(255,152,0,0.85);color:#fff;";
        DOM.stopBtn.style.cssText = baseBtnStyle + "background:rgba(244,67,54,0.85);color:#fff;";
        DOM.autoCloseBtn.style.cssText = baseBtnStyle + "background:rgba(96,125,139,0.6);color:#90a4ae;font-size:16px;padding:7px 10px;";
        DOM.autoCloseBtn.style.minWidth = "36px";
        DOM.autoCloseBtn.style.textAlign = "center";

        [DOM.saveBtn, DOM.parentBtn, DOM.stopBtn].forEach(b => {
            b.onmouseenter = () => {
                b.style.transform = "translateY(-1px)";
                b.style.filter = "brightness(1.15)";
                b.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
            };
            b.onmouseleave = () => {
                b.style.transform = "none";
                b.style.filter = "none";
                b.style.boxShadow = "none";
            };
            b.onmousedown = () => {
                b.style.transform = "translateY(0) scale(0.97)";
            };
            b.onmouseup = () => {
                b.style.transform = "translateY(-1px)";
            };
        });

        DOM.autoCloseBtn.onmouseenter = () => {
            DOM.autoCloseBtn.style.transform = "translateY(-1px)";
            DOM.autoCloseBtn.style.filter = "brightness(1.3)";
        };
        DOM.autoCloseBtn.onmouseleave = () => {
            if (!ui.autoCloseEnabled) {
                DOM.autoCloseBtn.style.transform = "none";
                DOM.autoCloseBtn.style.filter = "none";
            }
        };

        btnBox.appendChild(DOM.saveBtn);
        btnBox.appendChild(DOM.parentBtn);
        btnBox.appendChild(DOM.stopBtn);
        btnBox.appendChild(DOM.autoCloseBtn);
        DOM.body.appendChild(btnBox);

        DOM.resInfo = document.createElement("div");
        DOM.resInfo.style.cssText = "color:#4fc3f7;font-size:11px;font-weight:600;letter-spacing:0.3px;";
        DOM.resInfo.textContent = "[...]";
        DOM.body.appendChild(DOM.resInfo);

        DOM.queueInfo = document.createElement("div");
        DOM.queueInfo.style.cssText = "color:#ffeb3b;font-size:11px;font-weight:600;display:none;letter-spacing:0.3px;";
        DOM.body.appendChild(DOM.queueInfo);

        DOM.progressOuter = document.createElement("div");
        DOM.progressOuter.id = "eagle-progress";
        DOM.progressOuter.style.cssText = `
            width: 100%;
            height: 4px;
            background: rgba(255,255,255,0.08);
            border-radius: 2px;
            overflow: hidden;
            display: none;
        `;
        DOM.progressInner = document.createElement("div");
        DOM.progressInner.id = "eagle-progress-inner";
        DOM.progressInner.style.cssText = `
            width: 0%;
            height: 100%;
            background: linear-gradient(90deg, #4caf50, #66bb6a);
            border-radius: 2px;
            transition: width 0.1s linear;
        `;
        DOM.progressOuter.appendChild(DOM.progressInner);
        DOM.body.appendChild(DOM.progressOuter);

        DOM.panel.appendChild(DOM.body);

        setupDragAndDrop();

        document.body.appendChild(DOM.panel);

        try {
            const savedState = GM_getValue(KEYS.PANEL_STATE, null);
            if (savedState) applyPanelState(JSON.parse(savedState));
        } catch (e) {}

        ui.autoCloseEnabled = GM_getValue(KEYS.AUTO_CLOSE, false);
        updateAutoCloseBtn();
        if (ui.autoCloseEnabled && DOM.saveBtn) {
            DOM.saveBtn.textContent = "Save+Close";
            DOM.saveBtn.title = "Alt+Z — add to queue & close tab";
        }

        updatePostPageUI();
        updateColors();
    }

    function setupDragAndDrop() {
        let isDragging = false;
        let dragStartX = 0, dragStartY = 0;
        let panelStartLeft = 0, panelStartTop = 0;

        DOM.header.onmousedown = (e) => {
            if (e.target === DOM.collapseBtn) return;

            isDragging = true;
            dragStartX = e.clientX;
            dragStartY = e.clientY;
            panelStartLeft = DOM.panel.offsetLeft;
            panelStartTop = DOM.panel.offsetTop;

            DOM.panel.style.transform = "none";
            DOM.header.style.cursor = "grabbing";

            e.preventDefault();
            e.stopPropagation();
        };

        document.addEventListener("mousemove", (e) => {
            if (!isDragging) return;

            const dx = e.clientX - dragStartX;
            const dy = e.clientY - dragStartY;
            const newLeft = panelStartLeft + dx;
            const newTop = panelStartTop + dy;

            const panelRect = DOM.panel.getBoundingClientRect();
            const vw = window.innerWidth;
            const vh = window.innerHeight;

            DOM.panel.style.left = Math.max(0, Math.min(newLeft, vw - panelRect.width)) + "px";
            DOM.panel.style.top = Math.max(0, Math.min(newTop, vh - panelRect.height)) + "px";
        });

        document.addEventListener("mouseup", () => {
            if (isDragging) {
                isDragging = false;
                DOM.header.style.cursor = "";
                savePanelState();
            }
        });

        DOM.header.addEventListener("selectstart", (e) => e.preventDefault());
    }

    function updateColors() {
        const parentID = GM_getValue(KEYS.PARENT_ID, null);

        if (parentID) {
            DOM.saveBtn.style.background = "rgba(76,175,80,0.85)";
            DOM.parentBtn.style.background = "rgba(76,175,80,0.85)";
            DOM.parentBtn.textContent = "Parent: " + parentID;
        } else {
            DOM.saveBtn.style.background = "rgba(255,152,0,0.85)";
            DOM.parentBtn.style.background = "rgba(255,152,0,0.85)";
            DOM.parentBtn.textContent = "Parent";
        }

        if (DOM.resInfo) {
            DOM.resInfo.textContent = getImageResolution();
        }

        updateQueueUI();
        updateWorkerIndicator();
        saveUIState();
    }

    function updatePostPageUI() {
        const onPost = isPostPage();
        const opacity = onPost ? "1" : "0.35";
        const pointer = onPost ? "auto" : "none";

        if (DOM.saveBtn) {
            DOM.saveBtn.style.opacity = opacity;
            DOM.saveBtn.style.pointerEvents = pointer;
        }
        if (DOM.parentBtn) {
            DOM.parentBtn.style.opacity = opacity;
            DOM.parentBtn.style.pointerEvents = pointer;
        }
        if (DOM.stopBtn) {
            DOM.stopBtn.style.opacity = opacity;
            DOM.stopBtn.style.pointerEvents = pointer;
        }

        if (DOM.autoCloseBtn) {
            DOM.autoCloseBtn.style.opacity = "1";
            DOM.autoCloseBtn.style.pointerEvents = "auto";
        }

        if (!onPost) {
            if (DOM.resInfo) DOM.resInfo.textContent = "[not a post page]";
            if (DOM.queueInfo) DOM.queueInfo.style.display = "none";
        }

        saveUIState();
    }

    function updateQueueUI() {
        if (!DOM.queueInfo) return;

        const queue = getQueue();
        const processingText = worker.currentPostId
            ? ` | Processing: ${worker.currentPostId}`
            : '';

        if (queue.length > 0) {
            DOM.queueInfo.textContent = `${queue.length} in queue${processingText}`;
            DOM.queueInfo.style.display = "block";
        } else {
            DOM.queueInfo.style.display = "none";
        }
    }

    function updateWorkerIndicator() {
        if (!DOM.workerBadge) return;

        if (worker.isWorker) {
            DOM.workerBadge.textContent = "●";
            DOM.workerBadge.style.color = "#00e676";
            DOM.workerBadge.style.textShadow = "0 0 6px rgba(0,230,118,0.6)";
        } else {
            DOM.workerBadge.textContent = "○";
            DOM.workerBadge.style.color = "#555";
            DOM.workerBadge.style.textShadow = "none";
        }

        if (DOM.saveBtn) {
            if (worker.isWorker) {
                DOM.saveBtn.style.outline = "2px solid #00e676";
                DOM.saveBtn.style.outlineOffset = "2px";
            } else {
                DOM.saveBtn.style.outline = "none";
            }
        }
    }

    function updateAutoCloseBtn() {
        if (!DOM.autoCloseBtn) return;

        if (ui.autoCloseEnabled) {
            DOM.autoCloseBtn.style.background = "rgba(76,175,80,0.85)";
            DOM.autoCloseBtn.style.color = "#fff";
            DOM.autoCloseBtn.style.boxShadow = "0 0 10px rgba(76,175,80,0.4)";
        } else {
            DOM.autoCloseBtn.style.background = "rgba(96,125,139,0.6)";
            DOM.autoCloseBtn.style.color = "#90a4ae";
            DOM.autoCloseBtn.style.boxShadow = "none";
        }
    }

    function setCollapsed(collapsed) {
        ui.collapsed = collapsed;

        if (DOM.body) {
            DOM.body.style.maxHeight = collapsed ? "0" : "500px";
            DOM.body.style.opacity = collapsed ? "0" : "1";
            DOM.body.style.pointerEvents = collapsed ? "none" : "auto";
        }
        if (DOM.collapseBtn) {
            DOM.collapseBtn.style.transform = collapsed ? "rotate(0deg)" : "rotate(180deg)";
        }
    }

    function toggleCollapse() {
        setCollapsed(!ui.collapsed);
        savePanelState();
    }

    function showProgressBar(show) {
        if (!DOM.progressOuter || !DOM.progressInner) return;

        if (show) {
            if (progress.interval) {
                clearInterval(progress.interval);
                progress.interval = null;
            }

            DOM.progressOuter.style.display = "block";
            DOM.progressInner.style.width = "0%";

            if (worker.isWorker) {
                let width = 0;
                progress.interval = setInterval(() => {
                    width += CONFIG.PROGRESS_INCREMENT;
                    if (width >= CONFIG.PROGRESS_MAX_SHOW) {
                        clearInterval(progress.interval);
                        progress.interval = null;
                        return;
                    }
                    DOM.progressInner.style.width = width + "%";
                    saveProgressState(true, width);
                }, CONFIG.PROGRESS_INCREMENT_INTERVAL);
            } else {
                DOM.progressInner.style.width = "0%";
                saveProgressState(true, 0);
            }
        } else {
            if (progress.interval) {
                clearInterval(progress.interval);
                progress.interval = null;
            }

            DOM.progressInner.style.width = "100%";
            saveProgressState(false, 100);

            setTimeout(() => {
                DOM.progressOuter.style.display = "none";
                DOM.progressInner.style.width = "0%";
            }, CONFIG.PROGRESS_HIDE_DELAY);
        }
    }

    function bindButtons() {
        if (!DOM.saveBtn) return;

        DOM.saveBtn.onclick = async () => {
            const url = getOriginalImageURL();
            const tags = getTags();
            const postId = getPostID();

            if (!url) { toast("✗ No image URL"); return; }
            if (!postId) { toast("✗ No post ID"); return; }

            const added = await enqueueItem(url, tags, postId);
            if (added) {
                tryBecomeWorker();

                if (ui.autoCloseEnabled) {
                    waitForAutoClose(postId);
                }
            }
        };

        DOM.autoCloseBtn.onclick = () => {
            ui.autoCloseEnabled = !ui.autoCloseEnabled;
            GM_setValue(KEYS.AUTO_CLOSE, ui.autoCloseEnabled);
            updateAutoCloseBtn();
            toast(ui.autoCloseEnabled ? "Auto-close ON" : "Auto-close OFF");

            if (DOM.saveBtn) {
                if (ui.autoCloseEnabled) {
                    DOM.saveBtn.textContent = "Save+Close";
                    DOM.saveBtn.title = "Alt+Z — add to queue & close tab";
                } else {
                    DOM.saveBtn.textContent = "Save";
                    DOM.saveBtn.title = "Alt+Z — add to queue";
                }
            }

            saveUIState();
        };

        DOM.parentBtn.onclick = async () => {
            const id = getPostID();
            if (!id) { toast("✗ No post ID"); return; }

            GM_setValue(KEYS.PARENT_ID, id);
            updateColors();
            toast("Parent set: " + id);

            const url = getOriginalImageURL();
            const tags = getTags();
            if (!url) { toast("✗ No image URL"); return; }

            const added = await enqueueItem(url, tags, id);
            if (added) {
                tryBecomeWorker();

                if (ui.autoCloseEnabled) {
                    waitForAutoClose(id);
                }
            }
        };

        DOM.stopBtn.onclick = () => {
            GM_deleteValue(KEYS.PARENT_ID);
            resignWorker();
            clearQueue();
            updateColors();
            toast("Parent cleared, worker stopped & queue cleared");
        };
    }

    function setupHotkeys() {
        document.addEventListener("keydown", (e) => {
            if (!e.altKey) return;

            if (e.code === "KeyZ") {
                DOM.saveBtn?.click();
                e.preventDefault();
            }
            if (e.code === "KeyX") {
                DOM.parentBtn?.click();
                e.preventDefault();
            }
            if (e.code === "KeyC") {
                DOM.stopBtn?.click();
                e.preventDefault();
            }
            if (e.code === "KeyQ") {
                clearQueue();
                e.preventDefault();
            }
            if (e.code === "KeyS") {
                if (ui.autoCloseEnabled) {
                    DOM.saveBtn?.click();
                } else {
                    DOM.autoCloseBtn?.click();
                }
                e.preventDefault();
            }
        });
    }

    function showSettingsModal(callback) {
        if (document.getElementById("eagle-settings-modal")) return;

        const overlay = document.createElement("div");
        overlay.id = "eagle-settings-modal";
        overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:999999;display:flex;align-items:center;justify-content:center;";

        const modal = document.createElement("div");
        modal.style.cssText = "background:#1e1e1e;color:#e0e0e0;padding:24px;border-radius:12px;width:400px;max-width:90%;font-family:'Segoe UI',Tahoma,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,0.5);";

        modal.innerHTML = `
            <h3 style="margin:0 0 20px 0;font-size:18px;font-weight:600;">⚙️ Eagle Saver Settings</h3>
            <div style="margin-bottom:16px;">
                <label style="display:block;margin-bottom:6px;font-size:13px;color:#aaa;">API Token</label>
                <input id="eagle-settings-token" type="text" value="${GM_getValue(KEYS.API_TOKEN, CONFIG.DEFAULT_TOKEN)}"
                    style="width:100%;padding:8px 12px;background:#2d2d2d;border:1px solid #444;border-radius:6px;color:#e0e0e0;font-size:13px;box-sizing:border-box;"
                    placeholder="Your Eagle API token">
            </div>
            <div style="margin-bottom:20px;">
                <label style="display:block;margin-bottom:6px;font-size:13px;color:#aaa;">Port</label>
                <input id="eagle-settings-port" type="text" value="${GM_getValue(KEYS.API_PORT, CONFIG.DEFAULT_PORT)}"
                    style="width:100%;padding:8px 12px;background:#2d2d2d;border:1px solid #444;border-radius:6px;color:#e0e0e0;font-size:13px;box-sizing:border-box;"
                    placeholder="41595">
            </div>
            <div style="display:flex;gap:8px;justify-content:flex-end;align-items:center;">
                <button id="eagle-settings-reset-key"
                    style="padding:8px 16px;background:#f44336;color:white;border:none;border-radius:6px;cursor:pointer;font-size:13px;">Reset Token</button>
                <div style="flex:1;"></div>
                <button id="eagle-settings-cancel"
                    style="padding:8px 16px;background:#444;color:#e0e0e0;border:none;border-radius:6px;cursor:pointer;font-size:13px;">Cancel</button>
                <button id="eagle-settings-save"
                    style="padding:8px 16px;background:#4caf50;color:white;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:bold;">Save</button>
            </div>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);
        setTimeout(() => document.getElementById("eagle-settings-token").focus(), CONFIG.DEBOUNCE_RESET_DELAY);

        const handleEnter = (e) => {
            if (e.key === "Enter") document.getElementById("eagle-settings-save").click();
        };
        document.getElementById("eagle-settings-token").addEventListener("keydown", handleEnter);
        document.getElementById("eagle-settings-port").addEventListener("keydown", handleEnter);

        document.getElementById("eagle-settings-reset-key").onclick = () => {
            GM_deleteValue(KEYS.API_TOKEN);
            GM_deleteValue(KEYS.SETTINGS_DISMISSED);
            document.getElementById("eagle-settings-token").value = "";
            document.getElementById("eagle-settings-token").placeholder = "Token removed";
            document.getElementById("eagle-settings-token").focus();
            toast("⚠️ Token cleared");
        };

        document.getElementById("eagle-settings-cancel").onclick = () => {
            GM_setValue(KEYS.SETTINGS_DISMISSED, true);
            overlay.remove();
            if (callback) callback(false);
        };

        document.getElementById("eagle-settings-save").onclick = () => {
            const token = document.getElementById("eagle-settings-token").value.trim();
            const port = document.getElementById("eagle-settings-port").value.trim();

            if (!token || !port) {
                alert("Token and port are required!");
                return;
            }

            GM_setValue(KEYS.API_TOKEN, token);
            GM_setValue(KEYS.API_PORT, port);
            GM_setValue(KEYS.SETTINGS_DISMISSED, true);
            overlay.remove();

            if (callback) callback(true);
        };

        overlay.onclick = (e) => {
            if (e.target === overlay) {
                document.getElementById("eagle-settings-cancel").click();
            }
        };
    }

    function checkSettings() {
        const token = GM_getValue(KEYS.API_TOKEN, null);
        const port = GM_getValue(KEYS.API_PORT, null);
        const dismissed = GM_getValue(KEYS.SETTINGS_DISMISSED, false);

        if ((!token || !port) && !dismissed) {
            showSettingsModal((saved) => {
                if (saved) setTimeout(() => toast("Settings saved!"), CONFIG.SYNC_APPLY_DELAY);
            });
        }
    }

    function isRule34() { return location.hostname.includes("rule34"); }
    function isDanbooru() { return location.hostname.includes("danbooru"); }
    function isSankaku() { return location.hostname.includes("sankaku"); }
    function isGelbooru() { return location.hostname.includes("gelbooru"); }
    function isKonachan() { return location.hostname.includes("konachan"); }

    function isPostPage() {
        return getPostID() !== null;
    }

    function getPostID() {
        if (isRule34() || isGelbooru()) {
            return new URLSearchParams(location.search).get("id");
        }
        if (isDanbooru()) {
            const m = location.pathname.match(/posts\/(\d+)/);
            return m ? m[1] : null;
        }
        if (isSankaku()) {
            const m = location.pathname.match(/posts\/(\w+)/);
            return m ? m[1] : null;
        }
        if (isKonachan()) {
            const m = location.pathname.match(/post\/show\/(\d+)/);
            return m ? m[1] : null;
        }
        return null;
    }

    function getOriginalImageURL() {
        if (isRule34() || isGelbooru()) {
            let origLink = Array.from(document.querySelectorAll('li a, div a')).find(a =>
                /original image/i.test(a.textContent) ||
                (a.parentElement && /original image/i.test(a.parentElement.textContent))
            );
            if (origLink) return origLink.href;

            let img = document.querySelector("#image");
            if (img) {
                if (img.parentElement && img.parentElement.tagName === 'A') {
                    return img.parentElement.href;
                }
                return img.src;
            }
        }
        if (isDanbooru()) {
            let dl = document.querySelector("#post-option-download a, #image-download-link");
            if (dl) return dl.href;

            let img = document.querySelector("#image");
            if (img) return img.src;
        }

        if (isSankaku()) {
            let highres = document.querySelector('#highres');
            if (highres) {
                let url = highres.href;
                if (url.startsWith("./") || url.startsWith("/")) {
                    let img = document.querySelector("#image");
                    if (img) return img.src;
                }
                return url;
            }
            let img = document.querySelector("#image");
            if (img) return img.src;
        }
        if (isKonachan()) {
            let png = document.querySelector('a.png');
            if (png) return png.href;

            let highres = document.querySelector('#highres, #highres-show a');
            if (highres) return highres.href;

            let img = document.querySelector("#image");
            if (img) return img.src;
        }
        return null;
    }

    function getImageResolution() {
        if (isSankaku()) {
            const stats = document.querySelector('#stats');
            if (stats) {
                const m = stats.textContent.match(/Original:\s*(\d+)\s*[x×]\s*(\d+)/i);
                if (m) return `[${m[1]}x${m[2]}]`;
            }
        }
        if (isDanbooru()) {
            const sizeLi = Array.from(document.querySelectorAll('#post-information li'))
                .find(li => li.textContent.includes('Size'));
            if (sizeLi) {
                const m = sizeLi.textContent.match(/(\d+)\s*[x×]\s*(\d+)/);
                if (m) return `[${m[1]}x${m[2]}]`;
            }
        }
        if (isRule34() || isGelbooru() || isKonachan()) {
            const stats = document.querySelector('#stats, .sidebar, #tag-sidebar');
            if (stats) {
                const m = stats.textContent.match(/Size:\s*(\d+)\s*[x×]\s*(\d+)/i) ||
                          stats.textContent.match(/(\d+)\s*[x×]\s*(\d+)/);
                if (m) return `[${m[1]}x${m[2]}]`;
            }
        }
        const img = document.querySelector("#image");
        if (img && img.naturalWidth && img.naturalWidth > 500) {
            return `[${img.naturalWidth}x${img.naturalHeight}]`;
        }
        return "[Res: ?]";
    }

    function cleanTag(tag) {
        return tag.trim().replace(/_/g, " ").toLowerCase();
    }

    function getTags() {
        const tags = [];
        const artists = [];
        const seenArtists = new Set();
        const seenTags = new Set();

        const addArtist = (t) => {
            if (t && t !== "?" && !seenArtists.has(t)) {
                seenArtists.add(t);
                artists.push("artist:" + t);
            }
        };
        const addTag = (t) => {
            if (t && t !== "?" && !seenTags.has(t)) {
                seenTags.add(t);
                tags.push(t);
            }
        };

        if (isRule34()) {
            document.querySelectorAll("#tag-sidebar li a").forEach(a => {
                const t = cleanTag(a.textContent);
                const li = a.parentElement;
                const isArtist = li && li.classList.contains("tag-type-artist");
                if (isArtist) addArtist(t);
                else addTag(t);
            });
        }
        if (isDanbooru()) {
            document.querySelectorAll(".artist-tag-list li a").forEach(a => addArtist(cleanTag(a.textContent)));
            document.querySelectorAll(".general-tag-list li a, .character-tag-list li a, .copyright-tag-list li a, .meta-tag-list li a").forEach(a => addTag(cleanTag(a.textContent)));
        }
        if (isSankaku()) {
            document.querySelectorAll('#tag-sidebar li a').forEach(a => {
                const t = cleanTag(a.textContent);
                const li = a.parentElement;
                const isArtist = li && (
                    li.dataset.tagType === "artist" ||
                    li.classList.contains("tag-type-artist")
                );
                if (isArtist) addArtist(t);
                else addTag(t);
            });
        }
        if (isGelbooru() || isKonachan()) {
            document.querySelectorAll("li a").forEach(a => {
                const t = cleanTag(a.textContent);
                const li = a.parentElement;
                const type = li ? li.className.match(/tag-type-(\w+)/) : null;

                if (type && type[1] === "artist") addArtist(t);
                else if (type && ["general", "character", "copyright"].includes(type[1])) addTag(t);
                else addTag(t);
            });
        }

        const parentID = GM_getValue(KEYS.PARENT_ID, null);
        if (parentID) {
            tags.push("parent:" + parentID);
        }

        return [...new Set([...artists, ...tags])];
    }

    function init() {
        log('info', `Initializing v2.0.3, tab: ${TAB_ID}`);

        createPanel();

        bindButtons();
        setupHotkeys();

        setupValueChangeListeners();

        startWorkerRecoveryCheck();

        checkSettings();

        const queue = getQueue();
        if (queue.length > 0) {
            log('info', `Queue has ${queue.length} items, trying to become worker`);
            tryBecomeWorker();
        }

        let lastUrl = location.href;
        const urlObserver = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                updatePostPageUI();
                if (DOM.resInfo) {
                    DOM.resInfo.textContent = getImageResolution();
                }
            }
        });
        urlObserver.observe(document.body, { childList: true, subtree: true });

        document.addEventListener('visibilitychange', () => {
            if (document.visibilityState === 'visible') {
                log('info', 'Tab became visible — refreshing UI state');

                try {
                    const uiStateRaw = GM_getValue(KEYS.UI_STATE, null);
                    if (uiStateRaw) {
                        const uiState = JSON.parse(uiStateRaw);
                        applyUIState(uiState);
                    }
                } catch (e) {
                    log('warn', 'visibilitychange: UI_STATE parse error', e);
                }

                updatePostPageUI();
                updateQueueUI();
                updateWorkerIndicator();
                if (DOM.resInfo) {
                    const onPost = isPostPage();
                    const resolution = getImageResolution();
                    DOM.resInfo.textContent = onPost ? resolution : "[not a post page]";
                }
            }
        });

        window.addEventListener('focus', () => {
            log('info', 'Window focused — refreshing queue and worker info');
            updateQueueUI();
            updateWorkerIndicator();
        });
    }

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

    if (typeof GM_registerMenuCommand === "function") {
        GM_registerMenuCommand("Eagle Saver Settings", () => {
            showSettingsModal((saved) => {
                if (saved) toast("Settings saved!");
            });
        });
    }

    window.addEventListener('beforeunload', () => {
        log('info', 'Tab closing, cleaning up');

        stopHeartbeat();
        stopWorkerLoop();
        if (autoClose.timer) clearTimeout(autoClose.timer);
        if (progress.interval) clearInterval(progress.interval);

        if (worker.isWorker) {
            const active = getActiveWorker();
            if (active && active.tabId === TAB_ID) {
                GM_setValue(KEYS.WORKER, null);
                log('info', 'Worker status cleared');
            }
        }
    });

})();