Booru → Eagle cool Saver

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Booru → Eagle cool Saver
// @namespace    booru-eagle
// @version      1.4
// @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/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addValueChangeListener
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function () {
    "use strict";

    const API_TOKEN_KEY = "eagle_api_token";
    const API_PORT_KEY = "eagle_api_port";
    const SETTINGS_DISMISSED_KEY = "eagle_settings_dismissed";
    const DEFAULT_PORT = "41595";
    const DEFAULT_TOKEN = "Your Eagle API token here";
    const STORAGE_KEY = "eagle_parent_id";

    const QUEUE_KEY = "eagle_queue_v3";
    const WORKER_KEY = "eagle_worker_v3";
    const PANEL_STATE_KEY = "eagle_panel_state_v1";
    const AUTOCLOSE_KEY = "eagle_aut閉_v1";

    const TAB_ID = Date.now().toString(36) + Math.random().toString(36).substr(2, 6);
    const WORKER_TIMEOUT = 10000;
    const AUTOCLOSE_DELAY = 400; // мс после enqueue перед закрытием вкладки

    let saveBtn, parentBtn, stopBtn, autoCloseBtn, resInfo, queueInfo, workerBadge;
    let panelEl, panelHeader, panelBody, collapseBtn;
    let isWorker = false;
    let workerTimer = null;
    let isCollapsed = false;
    let panelSyncing = false;
    let autoCloseEnabled = false;

    // ==================== HELPERS ====================

    function getApiUrl() {
        const port = GM_getValue(API_PORT_KEY, DEFAULT_PORT);
        const token = GM_getValue(API_TOKEN_KEY, null);
        if (!token) return null;
        return `http://localhost:${port}/api/item/addFromURL?token=${token}`;
    }

    // ==================== PANEL STATE SYNC ====================

    function savePanelState() {
        if (!panelEl) return;
        const state = {
            left: panelEl.style.left,
            top: panelEl.style.top,
            transform: panelEl.style.transform,
            collapsed: isCollapsed
        };
        panelSyncing = true;
        GM_setValue(PANEL_STATE_KEY, JSON.stringify(state));
        setTimeout(() => { panelSyncing = false; }, 100);
    }

    function applyPanelState(state) {
        if (!state || !panelEl) return;
        panelEl.style.left = state.left;
        panelEl.style.top = state.top;
        panelEl.style.transform = state.transform;
        if (state.collapsed !== undefined && state.collapsed !== isCollapsed) {
            setCollapsed(state.collapsed);
        }
    }

    GM_addValueChangeListener(PANEL_STATE_KEY, (name, oldVal, newVal) => {
        if (panelSyncing) return;
        try {
            if (newVal) applyPanelState(JSON.parse(newVal));
        } catch (e) {}
    });

    // ==================== QUEUE ====================

    function getQueue() {
        try {
            const raw = GM_getValue(QUEUE_KEY, null);
            return raw ? JSON.parse(raw) : [];
        } catch (e) { return []; }
    }

    function saveQueue(queue) {
        GM_setValue(QUEUE_KEY, JSON.stringify(queue));
    }

    function enqueueItem(url, tags, postId) {
        if (!postId) return false;
        if (!url) return false;

        const queue = getQueue();
        if (queue.some(item => item.postId === postId)) {
            console.log(`[Eagle] Post ${postId} already in queue`);
            toast("⚠ Already in queue");
            return false;
        }

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

        saveQueue(queue);
        console.log(`[Eagle] Enqueued ${postId}, queue: ${queue.length}`);
        return true;
    }

    // ==================== WORKER SYSTEM ====================

    function getActiveWorker() {
        const w = GM_getValue(WORKER_KEY, null);
        if (!w) return null;
        if (Date.now() - w.heartbeat > WORKER_TIMEOUT) return null;
        return w;
    }

    function becomeWorker() {
        isWorker = true;
        GM_setValue(WORKER_KEY, { tabId: TAB_ID, heartbeat: Date.now() });
        console.log(`[Eagle] Tab ${TAB_ID} became WORKER`);
        updateWorkerIndicator();
    }

    function resignWorker() {
        isWorker = false;
        if (workerTimer) { clearInterval(workerTimer); workerTimer = null; }
        const w = GM_getValue(WORKER_KEY, null);
        if (w && w.tabId === TAB_ID) {
            GM_setValue(WORKER_KEY, null);
        }
        console.log(`[Eagle] Tab ${TAB_ID} resigned as worker`);
        updateWorkerIndicator();
    }

    function workerHeartbeat() {
        const w = GM_getValue(WORKER_KEY, null);
        if (w && w.tabId === TAB_ID) {
            w.heartbeat = Date.now();
            GM_setValue(WORKER_KEY, w);
        } else {
            isWorker = false;
            updateWorkerIndicator();
        }
    }

    function tryBecomeWorker() {
        if (isWorker) return;
        const active = getActiveWorker();
        if (!active) {
            becomeWorker();
            startWorkerLoop();
        }
    }

    function startWorkerLoop() {
        if (workerTimer) return;

        workerTimer = setInterval(() => {
            if (!isWorker) {
                if (workerTimer) { clearInterval(workerTimer); workerTimer = null; }
                return;
            }

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

            workerHeartbeat();
            processNextItem();

        }, 1500);

        setTimeout(() => {
            if (isWorker) {
                workerHeartbeat();
                processNextItem();
            }
        }, 300);
    }

    let currentlyProcessing = null;

    async function processNextItem() {
        if (!isWorker || currentlyProcessing) return;

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

        const item = queue[0];

        currentlyProcessing = item.postId;
        console.log(`[Eagle WORKER] Processing ${item.postId}`);
        updateQueueUI();

        await sendToEagle(item.url, item.tags);

        const q = getQueue();
        const idx = q.findIndex(i => i.postId === item.postId);
        if (idx !== -1) {
            q.splice(idx, 1);
            saveQueue(q);
        }

        currentlyProcessing = null;
        updateQueueUI();
    }

    // ==================== SYNC BETWEEN TABS ====================

    GM_addValueChangeListener(QUEUE_KEY, () => {
        updateQueueUI();
        if (isWorker) {
            setTimeout(() => processNextItem(), 500);
        }
    });

    GM_addValueChangeListener(WORKER_KEY, () => {
        if (!isWorker) {
            setTimeout(() => {
                if (!getActiveWorker() && getQueue().length > 0) {
                    tryBecomeWorker();
                }
            }, 1000);
        }
    });

    setInterval(() => {
        if (!isWorker) {
            const active = getActiveWorker();
            const queue = getQueue();
            if (!active && queue.length > 0) {
                tryBecomeWorker();
            }
        }
    }, 5000);

    // ==================== UI ====================

    function updateQueueUI() {
        if (!queueInfo) return;
        const queue = getQueue();
        if (queue.length > 0) {
            queueInfo.textContent = `${queue.length} in queue`;
            queueInfo.style.display = "block";
        } else {
            queueInfo.style.display = "none";
        }
    }

    function updateWorkerIndicator() {
        if (workerBadge) {
            if (isWorker) {
                workerBadge.textContent = "●";
                workerBadge.style.color = "#00e676";
                workerBadge.style.textShadow = "0 0 6px rgba(0,230,118,0.6)";
            } else {
                workerBadge.textContent = "○";
                workerBadge.style.color = "#555";
                workerBadge.style.textShadow = "none";
            }
        }
        if (saveBtn) {
            if (isWorker) {
                saveBtn.style.outline = "2px solid #00e676";
                saveBtn.style.outlineOffset = "2px";
            } else {
                saveBtn.style.outline = "none";
            }
        }
    }

    // ==================== PANEL ====================

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

    function toggleCollapse() {
        setCollapsed(!isCollapsed);
        savePanelState();
    }

    function createPanel() {
        if (document.getElementById("eagle-panel")) return;

        // Main panel
        panelEl = document.createElement("div");
        panelEl.id = "eagle-panel";
        panelEl.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;
        `;

        // Header (drag handle)
        panelHeader = document.createElement("div");
        panelHeader.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);
        `;
        panelHeader.onmouseenter = () => { panelEl.style.boxShadow = "0 16px 48px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06) inset"; };
        panelHeader.onmouseleave = () => { panelEl.style.boxShadow = "0 12px 40px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04) inset"; };

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

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

        // Collapse button
        collapseBtn = document.createElement("button");
        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);
        `;
        collapseBtn.textContent = "▾";
        collapseBtn.onmouseenter = () => { collapseBtn.style.color = "#fff"; collapseBtn.style.background = "rgba(255,255,255,0.08)"; };
        collapseBtn.onmouseleave = () => { collapseBtn.style.color = "#888"; collapseBtn.style.background = "none"; };
        collapseBtn.onclick = (e) => { e.stopPropagation(); toggleCollapse(); };

        panelHeader.appendChild(title);
        panelHeader.appendChild(workerBadge);
        panelHeader.appendChild(collapseBtn);
        panelEl.appendChild(panelHeader);

        // Body (collapsible)
        panelBody = document.createElement("div");
        panelBody.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;
        `;

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

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

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

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

        autoCloseBtn = document.createElement("button");
        autoCloseBtn.textContent = "⏻";
        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;";

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

        [saveBtn, parentBtn, 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)"; };
        });

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

        btnBox.appendChild(saveBtn);
        btnBox.appendChild(parentBtn);
        btnBox.appendChild(stopBtn);
        btnBox.appendChild(autoCloseBtn);
        panelBody.appendChild(btnBox);

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

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

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

        panelEl.appendChild(panelBody);

        // ===== Drag & Drop =====
        let isDragging = false, dragStartX = 0, dragStartY = 0, panelStartLeft = 0, panelStartTop = 0;

        panelHeader.onmousedown = (e) => {
            if (e.target === collapseBtn) return;
            isDragging = true;
            dragStartX = e.clientX;
            dragStartY = e.clientY;
            panelStartLeft = panelEl.offsetLeft;
            panelStartTop = panelEl.offsetTop;
            // Remove transform centering on first drag
            panelEl.style.transform = "none";
            panelEl.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;

            // Keep panel within viewport
            const panelRect = panelEl.getBoundingClientRect();
            const vw = window.innerWidth;
            const vh = window.innerHeight;

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

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

        // Prevent text selection during drag
        panelHeader.addEventListener("selectstart", (e) => e.preventDefault());

        document.body.appendChild(panelEl);

        // Load saved position
        try {
            const savedState = GM_getValue(PANEL_STATE_KEY, null);
            if (savedState) applyPanelState(JSON.parse(savedState));
        } catch (e) {}

        // Load auto-close state
        autoCloseEnabled = GM_getValue(AUTOCLOSE_KEY, false);
        updateAutoCloseBtn();

        // Обновить текст Save если auto-close включён
        if (autoCloseEnabled) {
            saveBtn.textContent = "Save+Close";
            saveBtn.title = "Alt+Z — add to queue & close tab";
        }

        updateColors();
        setInterval(updateColors, 1000);
    }

    function updateColors() {
        let parentID = GM_getValue(STORAGE_KEY, null);
        if (parentID) {
            saveBtn.style.background = "rgba(76,175,80,0.85)";
            parentBtn.style.background = "rgba(76,175,80,0.85)";
            parentBtn.textContent = "Parent: " + parentID;
        } else {
            saveBtn.style.background = "rgba(255,152,0,0.85)";
            parentBtn.style.background = "rgba(255,152,0,0.85)";
            parentBtn.textContent = "Parent";
        }
        if (resInfo) resInfo.textContent = getImageResolution();
        updateQueueUI();
        updateWorkerIndicator();
        updatePostPageUI();
    }

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

        saveBtn.style.opacity = opacity;
        saveBtn.style.pointerEvents = pointer;
        parentBtn.style.opacity = opacity;
        parentBtn.style.pointerEvents = pointer;
        stopBtn.style.opacity = opacity;
        stopBtn.style.pointerEvents = pointer;

        // Auto-close button stays active on ALL pages
        autoCloseBtn.style.opacity = "1";
        autoCloseBtn.style.pointerEvents = "auto";

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

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

    // ==================== BUTTON HANDLERS ====================

    function autoCloseAndSave() {
        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 = enqueueItem(url, tags, postId);
        if (added) {
            tryBecomeWorker();
            toast("✓ Queued — closing tab");
            setTimeout(() => {
                if (history.length > 1) {
                    history.back();
                } else {
                    window.close();
                }
            }, AUTOCLOSE_DELAY);
        }
    }

    function bindButtons() {
        saveBtn.onclick = () => {
            if (autoCloseEnabled) {
                autoCloseAndSave();
            } else {
                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 = enqueueItem(url, tags, postId);
                if (added) {
                    tryBecomeWorker();
                }
            }
        };

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

            // Обновить tooltip у Save
            if (autoCloseEnabled) {
                saveBtn.textContent = "Save+Close";
                saveBtn.title = "Alt+Z — add to queue & close tab";
            } else {
                saveBtn.textContent = "Save";
                saveBtn.title = "Alt+Z — add to queue";
            }
        };

        parentBtn.onclick = () => {
            const id = getPostID();
            if (!id) { toast("✗ No post ID"); return; }
            GM_setValue(STORAGE_KEY, id);
            updateColors();
            toast("Parent set: " + id);

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

            enqueueItem(url, tags, id);
            tryBecomeWorker();

            if (autoCloseEnabled) {
                toast("✓ Queued — going back");
                setTimeout(() => {
                    if (history.length > 1) {
                        history.back();
                    } else {
                        window.close();
                    }
                }, AUTOCLOSE_DELAY);
            }
        };

        stopBtn.onclick = () => {
            GM_deleteValue(STORAGE_KEY);
            resignWorker();
            updateColors();
            toast("Parent cleared & worker stopped");
        };
    }

    // ==================== HOTKEYS ====================

    document.addEventListener("keydown", e => {
        if (!e.altKey) return;
        if (e.code === "KeyZ") { saveBtn.click(); e.preventDefault(); }
        if (e.code === "KeyX") { parentBtn.click(); e.preventDefault(); }
        if (e.code === "KeyC") { stopBtn.click(); e.preventDefault(); }
        if (e.code === "KeyS") {
            if (autoCloseEnabled) { saveBtn.click(); }
            else { autoCloseBtn.click(); }
            e.preventDefault();
        }
    });

    // ==================== TOAST ====================

    function toast(text) {
        let 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(), 200);
        }, 2500);
    }

    // ==================== EAGLE API ====================

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

        const apiUrl = getApiUrl();
        if (!apiUrl) { toast("⚠️ No API token!"); return false; }

        showProgressBar(true);

        let finalUrl = url;
        if (isSankaku() && !url.startsWith('data:')) {
            finalUrl = await toDataURL(url);
        }

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

        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: apiUrl,
                headers: { "Content-Type": "application/json" },
                data: payload,
                onload: function(response) {
                    showProgressBar(false);
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.status === "success") {
                            toast("✓ Saved to Eagle");
                            resolve(true);
                        } else {
                            toast("✗ Eagle: " + (data.message || "error"));
                            console.error("[Eagle] API error:", data);
                            resolve(false);
                        }
                    } catch (e) {
                        console.error("[Eagle] Parse error:", e);
                        toast("✗ Response error");
                        resolve(false);
                    }
                },
                onerror: function(err) {
                    showProgressBar(false);
                    console.error("[Eagle] Network error:", err);
                    toast("✗ Network error");
                    resolve(false);
                },
                ontimeout: function() {
                    showProgressBar(false);
                    toast("✗ Timeout");
                    resolve(false);
                }
            });
        });
    }

    // ==================== SITE HELPERS ====================

    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"); }

    /**
     * Are we currently on a post page where saving is possible?
     */
    function isPostPage() {
        return getPostID() !== null;
    }

    // ==================== SETTINGS ====================

    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(API_TOKEN_KEY, 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(API_PORT_KEY, 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(), 100);

        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(API_TOKEN_KEY);
            GM_deleteValue(SETTINGS_DISMISSED_KEY);
            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(SETTINGS_DISMISSED_KEY, 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(API_TOKEN_KEY, token);
            GM_setValue(API_PORT_KEY, port);
            GM_setValue(SETTINGS_DISMISSED_KEY, 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(API_TOKEN_KEY, null);
        const port = GM_getValue(API_PORT_KEY, null);
        const dismissed = GM_getValue(SETTINGS_DISMISSED_KEY, false);
        if ((!token || !port) && !dismissed) {
            showSettingsModal((saved) => { if (saved) setTimeout(() => toast("Settings saved!"), 300); });
        }
    }

    // ==================== RESOLUTION ====================

    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: ?]";
    }

    // ==================== POST ID ====================

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

    // ==================== ORIGINAL IMAGE URL ====================

    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 && img.parentElement && img.parentElement.tagName === 'A') return img.parentElement.href;
            if (img) 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;
    }

    async function toDataURL(url) {
        try {
            const response = await fetch(url);
            const blob = await response.blob();
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onloadend = () => resolve(reader.result);
                reader.onerror = reject;
                reader.readAsDataURL(blob);
            });
        } catch (e) { console.error("Base64 conversion failed", e); return url; }
    }

    // ==================== TAGS ====================

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

    function getTags() {
        let tags = [], artists = [];
        if (isRule34()) {
            document.querySelectorAll("#tag-sidebar li.tag-type-artist a").forEach(a => { let t = cleanTag(a.textContent); if (t !== "?") artists.push("artist:" + t); });
            document.querySelectorAll("#tag-sidebar li a").forEach(a => { let t = cleanTag(a.textContent); if (t !== "?" && !artists.includes("artist:" + t)) tags.push(t); });
        }
        if (isDanbooru()) {
            document.querySelectorAll(".artist-tag-list li a").forEach(a => { let t = cleanTag(a.textContent); if (t !== "?") artists.push("artist:" + t); });
            document.querySelectorAll(".general-tag-list li a, .character-tag-list li a, .copyright-tag-list li a, .meta-tag-list li a").forEach(a => { let t = cleanTag(a.textContent); if (t !== "?") tags.push(t); });
        }
        if (isSankaku()) {
            document.querySelectorAll('#tag-sidebar li a[data-tag-type="artist"], #tag-sidebar li.tag-type-artist a').forEach(a => { let t = cleanTag(a.textContent); if (t !== "?") artists.push("artist:" + t); });
            document.querySelectorAll('#tag-sidebar li a').forEach(a => { let t = cleanTag(a.textContent); if (t !== "?" && !artists.includes("artist:" + t)) tags.push(t); });
        }
        if (isGelbooru() || isKonachan()) {
            document.querySelectorAll("li.tag-type-artist a").forEach(a => { let t = cleanTag(a.textContent); if (t !== "?") artists.push("artist:" + t); });
            document.querySelectorAll("li.tag-type-general a, li.tag-type-character a, li.tag-type-copyright a").forEach(a => { let t = cleanTag(a.textContent); if (t !== "?") tags.push(t); });
        }
        let parentID = GM_getValue(STORAGE_KEY, null);
        if (parentID) tags.push("parent:" + parentID);
        return [...new Set([...artists, ...tags])];
    }

    // ==================== PROGRESS BAR ====================

    function showProgressBar(show) {
        const outer = document.getElementById("eagle-progress");
        if (!outer) return;
        const inner = document.getElementById("eagle-progress-inner");
        if (!inner) return;

        if (show) {
            outer.style.display = "block";
            inner.style.width = "0%";
            let width = 0;
            if (outer._interval) clearInterval(outer._interval);
            outer._interval = setInterval(() => {
                width += 5;
                if (width >= 90) { clearInterval(outer._interval); return; }
                inner.style.width = width + "%";
            }, 50);
        } else {
            if (outer._interval) clearInterval(outer._interval);
            inner.style.width = "100%";
            setTimeout(() => { outer.style.display = "none"; inner.style.width = "0%"; }, 400);
        }
    }

    // ==================== INIT ====================

    setTimeout(createPanel, 1500);
    setTimeout(bindButtons, 1600);
    checkSettings();

    setTimeout(() => {
        const queue = getQueue();
        if (queue.length > 0) {
            tryBecomeWorker();
        }
    }, 2000);

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

    window.addEventListener('beforeunload', () => {
        if (isWorker) {
            const w = GM_getValue(WORKER_KEY, null);
            if (w && w.tabId === TAB_ID) {
                GM_setValue(WORKER_KEY, null);
            }
        }
    });

})();