Save imgs from booru directly to Eagle cool (single-worker queue, draggable panel)
// ==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);
}
}
});
})();