// ==UserScript==
// @name GPT Image 内部网页跳转抠图
// @namespace https://local.codex/gpt-img-to-koukoutu
// @version 1.1
// @description Send images from custom source websites to koukoutu remove-bg page, then auto start.
// @author 木木
// @match https://*/*
// @match http://*/*
// @match https://www.koukoutu.com/*
// @noframes
// @connect *
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_addValueChangeListener
// @grant GM_registerMenuCommand
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const STORE_KEY = 'gpt_img_to_koukoutu_pending_image';
const SOURCE_HOSTS_KEY = 'gpt_img_to_koukoutu_source_hosts';
const KOUKOUTU_BASE_URL = 'https://www.koukoutu.com/removebgtool/all';
const KOUKOUTU_TAB_NAME = 'gpt_image_to_koukoutu_tab';
const MAX_AGE_MS = 30 * 60 * 1000;
const DEFAULT_SOURCE_HOSTS = [
'gpt-img-2.hf.space',
'www.xiaohongshu.com',
'xiaohongshu.com'
];
const TXT = {
ready: '\u51c6\u5907\u5c31\u7eea',
sourceTitle: '\u53d1\u9001\u5230\u62a0\u62a0\u56fe',
sourceHint: 'Alt+K \u53ef\u5728\u5927\u56fe\u9884\u89c8\u65f6\u76f4\u63a5\u53d1\u9001',
sendCurrent: '\u53d1\u9001\u5f53\u524d\u56fe',
overlaySend: '\u53d1\u9001\u5230\u62a0\u62a0\u56fe',
openKoukoutu: '\u6253\u5f00\u62a0\u56fe',
finding: '\u6b63\u5728\u5bfb\u627e\u751f\u6210\u56fe...',
noImage: '\u6ca1\u627e\u5230\u751f\u6210\u56fe\u3002\u8bf7\u7b49\u56fe\u7247\u751f\u6210\u5b8c\u6210\u540e\u518d\u8bd5\u3002',
reading: '\u6b63\u5728\u8bfb\u53d6\u56fe\u7247...',
saved: '\u5df2\u4fdd\u5b58\uff0c\u6b63\u5728\u6253\u5f00\u62a0\u56fe\u9875...',
candidate: '\u5df2\u8bc6\u522b\u5019\u9009\u56fe',
targetTitle: '\u63a5\u6536 GPT \u56fe\u7247',
importPending: '\u5bfc\u5165\u5f85\u5904\u7406',
clear: '\u6e05\u7a7a',
waitingInput: '\u7b49\u5f85\u4e0a\u4f20\u5165\u53e3',
imported: '\u5df2\u5bfc\u5165\u56fe\u7247\uff0c\u6b63\u5728\u5c1d\u8bd5\u81ea\u52a8\u70b9\u51fb\u5f00\u59cb\u62a0\u56fe...',
noInput: '\u6ca1\u627e\u5230\u4e0a\u4f20\u5165\u53e3\uff0c\u8bf7\u5237\u65b0\u9875\u9762\u540e\u518d\u70b9\u5bfc\u5165\u3002',
noPending: '\u6ca1\u6709\u5f85\u5904\u7406\u56fe\u7247\uff0c\u6216\u56fe\u7247\u5df2\u8d85\u8fc7 30 \u5206\u949f\u3002',
pendingFound: '\u68c0\u6d4b\u5230\u5f85\u5904\u7406\u56fe\u7247\uff0c\u51c6\u5907\u81ea\u52a8\u5bfc\u5165...',
cleared: '\u5df2\u6e05\u7a7a\u5f85\u5904\u7406\u56fe\u7247\u3002',
startClicked: '\u5df2\u70b9\u51fb\u5f00\u59cb\u62a0\u56fe\u3002',
noStart: '\u5df2\u5bfc\u5165\uff0c\u4f46\u6ca1\u627e\u5230\u53ef\u70b9\u51fb\u7684\u5f00\u59cb\u6309\u94ae\u3002',
loginPopup: '\u68c0\u6d4b\u5230\u767b\u5f55\u5f39\u7a97\uff0c\u5df2\u5c1d\u8bd5\u5173\u95ed\u5e76\u7b49\u5f85\u9875\u9762\u6062\u590d...',
loginStill: '\u767b\u5f55\u5f39\u7a97\u4ecd\u7136\u51fa\u73b0\uff0c\u8bf7\u624b\u52a8\u5173\u95ed\u540e\u70b9\u201c\u5f00\u59cb\u62a0\u56fe\u201d\u3002',
addTitle: '\u6dfb\u52a0\u5230\u62a0\u56fe\u6765\u6e90',
addHint: '\u6dfb\u52a0\u672c\u7ad9\u540e\uff0c\u6b64\u7f51\u7ad9\u4f1a\u663e\u793a\u53d1\u9001\u9762\u677f',
addSite: '\u6dfb\u52a0\u672c\u7ad9',
hidePanel: '\u9690\u85cf',
siteAdded: '\u5df2\u6dfb\u52a0\u672c\u7ad9\uff0c\u5373\u5c06\u5207\u6362\u5230\u53d1\u9001\u9762\u677f\u3002'
};
const isTargetPage = location.hostname === 'www.koukoutu.com';
let targetImporting = false;
function ready(fn) {
if (document.body) {
fn();
return;
}
const timer = setInterval(() => {
if (!document.body) return;
clearInterval(timer);
fn();
}, 100);
}
function normalizeHost(host) {
return String(host || '').trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
}
function uniqueHosts(hosts) {
return Array.from(new Set(hosts.map(normalizeHost).filter(Boolean))).sort();
}
async function getSourceHosts() {
const saved = await GM_getValue(SOURCE_HOSTS_KEY, null);
if (Array.isArray(saved) && saved.length) return uniqueHosts(saved);
return uniqueHosts(DEFAULT_SOURCE_HOSTS);
}
async function setSourceHosts(hosts) {
const next = uniqueHosts(hosts);
await GM_setValue(SOURCE_HOSTS_KEY, next);
return next;
}
function hostMatchesPattern(host, pattern) {
const cleanHost = normalizeHost(host);
const cleanPattern = normalizeHost(pattern);
if (!cleanHost || !cleanPattern) return false;
if (cleanPattern === '*') return true;
if (cleanPattern.startsWith('*.')) {
const root = cleanPattern.slice(2);
return cleanHost === root || cleanHost.endsWith(`.${root}`);
}
return cleanHost === cleanPattern;
}
async function isAllowedSourceHost(host) {
if (isTargetPage) return false;
const hosts = await getSourceHosts();
return hosts.some(pattern => hostMatchesPattern(host, pattern));
}
function registerMenuCommands() {
if (typeof GM_registerMenuCommand !== 'function') return;
const currentHost = normalizeHost(location.hostname);
if (!currentHost || isTargetPage) return;
GM_registerMenuCommand('\u6dfb\u52a0\u5f53\u524d\u7f51\u7ad9\u4e3a\u6765\u6e90', async () => {
const hosts = await getSourceHosts();
if (!hosts.some(pattern => hostMatchesPattern(currentHost, pattern))) {
await setSourceHosts([...hosts, currentHost]);
}
alert(`\u5df2\u6dfb\u52a0\uff1a${currentHost}\n\u5237\u65b0\u9875\u9762\u540e\u663e\u793a\u53d1\u9001\u9762\u677f\u3002`);
});
GM_registerMenuCommand('\u79fb\u9664\u5f53\u524d\u7f51\u7ad9\u6765\u6e90', async () => {
const hosts = await getSourceHosts();
const next = hosts.filter(pattern => !hostMatchesPattern(currentHost, pattern));
await setSourceHosts(next);
alert(`\u5df2\u79fb\u9664\uff1a${currentHost}\n\u5237\u65b0\u9875\u9762\u540e\u751f\u6548\u3002`);
});
GM_registerMenuCommand('\u67e5\u770b\u5df2\u6dfb\u52a0\u7f51\u7ad9', async () => {
const hosts = await getSourceHosts();
alert(`\u5df2\u6dfb\u52a0\u7684\u6765\u6e90\u7f51\u7ad9\uff1a\n\n${hosts.join('\n')}`);
});
GM_registerMenuCommand('\u91cd\u7f6e\u9ed8\u8ba4\u6765\u6e90\u7f51\u7ad9', async () => {
await setSourceHosts(DEFAULT_SOURCE_HOSTS);
alert('\u5df2\u91cd\u7f6e\u4e3a\u9ed8\u8ba4\u6765\u6e90\u7f51\u7ad9\u3002');
});
}
function installStyle() {
const css = `
.g2k-panel {
position: fixed !important;
right: 16px !important;
bottom: 18px !important;
z-index: 2147483647 !important;
width: 248px;
padding: 12px;
border: 1px solid rgba(28, 25, 23, .12);
border-radius: 14px;
background: rgba(255, 255, 255, .97);
box-shadow: 0 18px 56px rgba(15, 23, 42, .22);
color: #1c1917;
font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
backdrop-filter: blur(10px);
pointer-events: auto !important;
}
.g2k-title {
margin: 0 0 6px;
font-size: 13px;
font-weight: 700;
}
.g2k-hint {
margin: 0 0 8px;
color: #78716c;
font-size: 11px;
}
.g2k-row {
display: flex;
gap: 8px;
margin-top: 8px;
}
.g2k-btn {
flex: 1;
border: 0;
border-radius: 10px;
padding: 8px 9px;
background: #18181b;
color: #fff;
cursor: pointer;
font-size: 12px;
font-weight: 650;
}
.g2k-btn.secondary {
background: #f4f4f5;
color: #27272a;
}
.g2k-btn:disabled {
cursor: not-allowed;
opacity: .55;
}
.g2k-status {
min-height: 18px;
color: #57534e;
word-break: break-word;
}
.g2k-overlay-send {
position: fixed !important;
right: 16px !important;
top: 52px !important;
z-index: 2147483647 !important;
border: 0;
border-radius: 999px;
padding: 9px 14px;
background: #ffffff;
color: #18181b;
box-shadow: 0 10px 34px rgba(0, 0, 0, .28);
cursor: pointer;
font: 12px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
font-weight: 700;
pointer-events: auto !important;
}
`;
try {
GM_addStyle(css);
} catch {
const style = document.createElement('style');
style.textContent = css;
document.documentElement.appendChild(style);
}
}
function makePanel(title, hint) {
installStyle();
document.querySelectorAll('.g2k-panel').forEach(el => el.remove());
const panel = document.createElement('div');
panel.className = 'g2k-panel';
panel.innerHTML = `
${title}
${hint ? `${hint}
` : ''}
${TXT.ready}
`;
document.body.appendChild(panel);
return {
root: panel,
status: panel.querySelector('.g2k-status'),
row: panel.querySelector('.g2k-row'),
setStatus(text) {
this.status.textContent = text;
},
addButton(text, className, onClick) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `g2k-btn ${className || ''}`.trim();
btn.textContent = text;
btn.addEventListener('click', onClick);
this.row.appendChild(btn);
return btn;
}
};
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function openKoukoutu() {
window.open(`${KOUKOUTU_BASE_URL}#from-gpt-img-${Date.now()}`, KOUKOUTU_TAB_NAME);
}
function isVisible(el) {
if (!el || !el.isConnected) return false;
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || 1) > 0;
}
function visibleArea(el) {
const rect = el.getBoundingClientRect();
if (rect.width < 80 || rect.height < 80) return 0;
if (rect.bottom < 0 || rect.right < 0 || rect.top > innerHeight || rect.left > innerWidth) return 0;
return Math.round(rect.width * rect.height);
}
function normalizeUrl(url) {
try {
return new URL(url, location.href).toString();
} catch {
return url;
}
}
function findBestImage() {
const viewportArea = Math.max(1, innerWidth * innerHeight);
const candidates = [];
for (const img of document.querySelectorAll('img')) {
const area = visibleArea(img);
const src = img.currentSrc || img.src || '';
if (!area || !src) continue;
const lower = src.toLowerCase();
if (/\b(logo|favicon|icon|avatar)\b/.test(lower)) continue;
if (img.naturalWidth < 128 || img.naturalHeight < 128) continue;
const rect = img.getBoundingClientRect();
const centerDx = Math.abs(rect.left + rect.width / 2 - innerWidth / 2) / innerWidth;
const centerDy = Math.abs(rect.top + rect.height / 2 - innerHeight / 2) / innerHeight;
let score = area - (centerDx + centerDy) * 100000;
const alt = (img.alt || '').toLowerCase();
if (alt.includes('generated result')) score += 100000000;
if (area / viewportArea > 0.22) score += 80000000;
if (src.startsWith('data:image/')) score += 50000000;
if (img.closest('button[aria-label*="\u9884\u89c8"], button[class*="cursor-zoom"]')) score += 1000000;
if (/\/images\/|logo|favicon|icon/.test(lower)) score -= 50000000;
candidates.push({
type: 'img',
el: img,
src: normalizeUrl(src),
score,
area,
width: img.naturalWidth,
height: img.naturalHeight
});
}
for (const canvas of document.querySelectorAll('canvas')) {
const area = visibleArea(canvas);
if (!area || canvas.width < 128 || canvas.height < 128) continue;
candidates.push({
type: 'canvas',
el: canvas,
score: area + 100000,
area,
width: canvas.width,
height: canvas.height
});
}
candidates.sort((a, b) => b.score - a.score);
return candidates[0] || null;
}
function gmBlob(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload(resp) {
if (resp.status >= 200 && resp.status < 300 && resp.response) {
resolve(resp.response);
} else {
reject(new Error(`HTTP ${resp.status}`));
}
},
onerror() {
reject(new Error('network error'));
},
ontimeout() {
reject(new Error('timeout'));
}
});
});
}
function dataUrlToBlob(dataUrl) {
const [head, body] = dataUrl.split(',');
const mime = (head.match(/data:([^;]+)/) || [])[1] || 'image/png';
const bin = atob(body);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i);
return new Blob([bytes], { type: mime });
}
function blobToDataUrl(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error || new Error('read image failed'));
reader.readAsDataURL(blob);
});
}
function canvasToBlob(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob(blob => {
if (blob) resolve(blob);
else reject(new Error('canvas is blocked by browser security'));
}, 'image/png');
});
}
async function candidateToBlob(candidate) {
if (candidate.type === 'canvas') return canvasToBlob(candidate.el);
const src = candidate.src;
if (src.startsWith('data:image/')) return dataUrlToBlob(src);
if (src.startsWith('blob:')) return fetch(src).then(resp => resp.blob());
return gmBlob(src);
}
function dataUrlToFile(dataUrl, name) {
const blob = dataUrlToBlob(dataUrl);
const ext = blob.type.includes('jpeg') ? 'jpg' : blob.type.includes('webp') ? 'webp' : 'png';
return new File([blob], name || `gpt-image-${Date.now()}.${ext}`, { type: blob.type || 'image/png' });
}
async function getPendingImage() {
const payload = await GM_getValue(STORE_KEY, null);
if (!payload || !payload.dataUrl || Date.now() - payload.createdAt > MAX_AGE_MS) return null;
return payload;
}
async function sendCurrentImage(panel, btn) {
if (btn) btn.disabled = true;
try {
panel.setStatus(TXT.finding);
const candidate = findBestImage();
if (!candidate) throw new Error(TXT.noImage);
panel.setStatus(`${TXT.reading} ${candidate.width}x${candidate.height}`);
const blob = await candidateToBlob(candidate);
const dataUrl = await blobToDataUrl(blob);
await GM_setValue(STORE_KEY, {
dataUrl,
name: `gpt-image-${new Date().toISOString().replace(/[:.]/g, '-')}.png`,
type: blob.type || 'image/png',
createdAt: Date.now()
});
panel.setStatus(TXT.saved);
openKoukoutu();
} catch (err) {
panel.setStatus(err && err.message ? err.message : String(err));
} finally {
if (btn) btn.disabled = false;
}
}
function findLightboxHost(img) {
if (!img) return null;
const minArea = innerWidth * innerHeight * 0.22;
if (visibleArea(img.el) < minArea) return null;
let host = img.el.parentElement;
let best = null;
while (host && host !== document.body) {
const rect = host.getBoundingClientRect();
const style = getComputedStyle(host);
const coversScreen = rect.width > innerWidth * 0.65 && rect.height > innerHeight * 0.65;
if (style.position === 'fixed' || style.position === 'sticky' || coversScreen || host.getAttribute('role') === 'dialog') {
best = host;
}
host = host.parentElement;
}
return best || document.body;
}
function ensureOverlayButton(panel) {
const img = findBestImage();
const host = img ? findLightboxHost(img) : null;
const existing = document.querySelector('.g2k-overlay-send');
if (!host) {
if (existing) existing.remove();
return;
}
if (existing) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'g2k-overlay-send';
btn.textContent = TXT.overlaySend;
btn.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
sendCurrentImage(panel, btn);
});
host.appendChild(btn);
}
async function startSourceUI() {
const panel = makePanel(TXT.sourceTitle, TXT.sourceHint);
const sendBtn = panel.addButton(TXT.sendCurrent, '', () => sendCurrentImage(panel, sendBtn));
panel.addButton(TXT.openKoukoutu, 'secondary', () => {
openKoukoutu();
});
const onShortcut = event => {
const isAltK = event.altKey && !event.ctrlKey && !event.metaKey && event.key.toLowerCase() === 'k';
const isCtrlShiftK = event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'k';
if (!isAltK && !isCtrlShiftK) return;
event.preventDefault();
event.stopPropagation();
sendCurrentImage(panel, sendBtn);
};
window.addEventListener('keydown', onShortcut, true);
document.addEventListener('keydown', onShortcut, true);
setInterval(() => {
const img = findBestImage();
if (img) panel.setStatus(`${TXT.candidate}: ${img.width}x${img.height}`);
ensureOverlayButton(panel);
}, 1000);
}
async function startAddSiteUI() {
const currentHost = normalizeHost(location.hostname);
if (!currentHost) return;
const panel = makePanel(TXT.addTitle, TXT.addHint);
panel.setStatus(currentHost);
panel.addButton(TXT.addSite, '', async () => {
const hosts = await getSourceHosts();
if (!hosts.some(pattern => hostMatchesPattern(currentHost, pattern))) {
await setSourceHosts([...hosts, currentHost]);
}
panel.setStatus(TXT.siteAdded);
await sleep(500);
startSourceUI();
});
panel.addButton(TXT.hidePanel, 'secondary', () => {
panel.root.remove();
});
}
function getImageInputs() {
return Array.from(document.querySelectorAll('input[type="file"]'))
.filter(input => String(input.accept || '').includes('image') || input.multiple);
}
function normalizedText(el) {
return (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
}
function findLoginDialog() {
const nodes = Array.from(document.querySelectorAll('body *'));
return nodes.find(el => {
if (!isVisible(el)) return false;
const rect = el.getBoundingClientRect();
if (rect.width < 260 || rect.height < 180) return false;
const text = normalizedText(el);
return text.includes('\u8bf7\u767b\u5f55') &&
(text.includes('\u5fae\u4fe1\u626b\u7801\u767b\u5f55') || text.includes('\u5207\u6362\u4e3a\u624b\u673a\u53f7\u767b\u5f55'));
}) || null;
}
function closeLoginDialog() {
const dialog = findLoginDialog();
if (!dialog) return false;
const closeCandidates = Array.from(document.querySelectorAll('button, [role="button"], a, div, span')).filter(el => {
if (!isVisible(el)) return false;
const text = normalizedText(el);
const aria = `${el.getAttribute('aria-label') || ''} ${el.getAttribute('title') || ''}`.toLowerCase();
if (text === '\u00d7' || text === 'x' || text === 'X' || text.includes('\u5173\u95ed') || aria.includes('close')) return true;
const d = dialog.getBoundingClientRect();
const r = el.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
return cx >= d.right - 35 && cx <= d.right + 45 && cy >= d.top - 45 && cy <= d.top + 35;
});
const target = closeCandidates[0];
if (target) {
target.click();
return true;
}
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, cancelable: true }));
return true;
}
function isDisabledLike(el) {
const className = String(el.className || '');
return Boolean(
el.disabled ||
el.getAttribute('disabled') !== null ||
el.getAttribute('aria-disabled') === 'true' ||
/\b(disabled|cursor-not-allowed|opacity-50|opacity-40)\b/.test(className)
);
}
function findStartButton() {
const terms = [
'\u5f00\u59cb\u62a0\u56fe',
'\u5f00\u59cb\u5904\u7406',
'\u7acb\u5373\u62a0\u56fe',
'\u4e00\u952e\u62a0\u56fe',
'\u5f00\u59cb\u53bb\u80cc\u666f',
'\u5f00\u59cb'
];
const blockTerms = [
'\u5386\u53f2',
'\u4e0a\u4f20',
'\u4e0b\u8f7d',
'\u6e05\u7a7a',
'\u5220\u9664',
'\u767b\u5f55'
];
const nodes = Array.from(document.querySelectorAll('button, a, [role="button"], div[class*="cursor-pointer"]'));
return nodes.find(el => {
if (!isVisible(el) || isDisabledLike(el)) return false;
const text = normalizedText(el);
if (!text || text.length > 30) return false;
if (blockTerms.some(term => text.includes(term))) return false;
return terms.some(term => text.includes(term));
}) || null;
}
async function waitAndClickStartButton(panel) {
for (let i = 0; i < 90; i += 1) {
if (findLoginDialog()) {
closeLoginDialog();
panel.setStatus(TXT.loginPopup);
await sleep(2500);
continue;
}
const btn = findStartButton();
if (btn) {
btn.scrollIntoView({ block: 'center', inline: 'center' });
await sleep(200);
btn.click();
await sleep(1400);
if (findLoginDialog()) {
closeLoginDialog();
panel.setStatus(TXT.loginPopup);
await sleep(2000);
const retryBtn = findStartButton();
if (retryBtn && !findLoginDialog()) {
retryBtn.scrollIntoView({ block: 'center', inline: 'center' });
await sleep(200);
retryBtn.click();
await sleep(1400);
}
if (findLoginDialog()) {
closeLoginDialog();
panel.setStatus(TXT.loginStill);
return false;
}
}
panel.setStatus(TXT.startClicked);
return true;
}
await sleep(500);
}
panel.setStatus(TXT.noStart);
return false;
}
async function importToKoukoutu(payload, panel) {
const file = dataUrlToFile(payload.dataUrl, payload.name);
panel.setStatus(`${TXT.waitingInput}: ${file.name}`);
for (let i = 0; i < 80; i += 1) {
const input = getImageInputs()[0];
if (input) {
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
panel.setStatus(TXT.imported);
await GM_deleteValue(STORE_KEY);
await sleep(1500);
await waitAndClickStartButton(panel);
return true;
}
await sleep(250);
}
panel.setStatus(TXT.noInput);
return false;
}
async function startTargetUI() {
const panel = makePanel(TXT.targetTitle);
let lastImportedAt = 0;
const importBtn = panel.addButton(TXT.importPending, '', async () => {
if (targetImporting) return;
targetImporting = true;
importBtn.disabled = true;
try {
const payload = await getPendingImage();
if (!payload) {
panel.setStatus(TXT.noPending);
return;
}
if (payload.createdAt && payload.createdAt === lastImportedAt) return;
lastImportedAt = payload.createdAt || Date.now();
await importToKoukoutu(payload, panel);
} catch (err) {
panel.setStatus(err && err.message ? err.message : String(err));
} finally {
targetImporting = false;
importBtn.disabled = false;
}
});
panel.addButton(TXT.clear, 'secondary', async () => {
await GM_deleteValue(STORE_KEY);
panel.setStatus(TXT.cleared);
});
const triggerImport = async () => {
const payload = await getPendingImage();
if (!payload) return false;
if (payload.createdAt && payload.createdAt === lastImportedAt) return false;
panel.setStatus(TXT.pendingFound);
await sleep(500);
importBtn.click();
return true;
};
if (typeof GM_addValueChangeListener === 'function') {
GM_addValueChangeListener(STORE_KEY, (_name, _oldValue, newValue) => {
if (!newValue || !newValue.dataUrl) return;
triggerImport();
});
}
window.addEventListener('focus', () => {
triggerImport();
});
window.addEventListener('hashchange', () => {
triggerImport();
});
const payload = await getPendingImage();
if (payload) {
panel.setStatus(TXT.pendingFound);
await sleep(800);
importBtn.click();
} else {
panel.setStatus(TXT.noPending);
}
}
ready(async () => {
console.log('[GPT Image to Koukoutu] userscript loaded:', location.href);
registerMenuCommands();
if (isTargetPage) {
startTargetUI();
} else if (await isAllowedSourceHost(location.hostname)) {
startSourceUI();
} else {
startAddSiteUI();
}
});
})();