// ==UserScript==
// @name 即梦AI左侧大图下载按钮
// @namespace https://jimeng.jianying.com/
// @version 0.1.0
// @description 在即梦AI资产页添加“下载左侧大图”按钮,一键下载左侧大图(优先WebP)
// @author Nick Liu
// @match https://jimeng.jianying.com/ai-tool/*
// @icon data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256"/>
// @grant GM_download
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ========= 配置 =========
const UI_POS = { top: '90px', right: '18px' }; // 按钮位置(固定定位)
const SCAN_DEBOUNCE_MS = 600; // 变更后扫描的防抖时间
const MIN_ABS_AREA = 60000; // 候选元素最小像素面积下限
const MIN_VIEWPORT_AREA_RATIO = 0.04; // 最小面积阈值占视窗比例
// ========================
let lastBest = null; // { url, area, left, isWebp, w, h, tag }
let scanTimer = null;
function log(...args) {
// console.debug('[即梦大图DL]', ...args);
}
function ensureButton() {
let btn = document.getElementById('jm-left-big-image-download-btn');
if (btn) return btn;
btn = document.createElement('button');
btn.id = 'jm-left-big-image-download-btn';
btn.textContent = '下载左侧大图';
btn.title = '下载页面左侧显示的大图(优先 WebP)';
btn.style.cssText = `
position: fixed;
z-index: 2147483647;
top: ${UI_POS.top};
right: ${UI_POS.right};
padding: 10px 14px;
border-radius: 8px;
background: #1f6feb;
color: #fff;
border: none;
font-size: 14px;
line-height: 1;
cursor: pointer;
box-shadow: 0 6px 18px rgba(0,0,0,.15);
opacity: .95;
`;
btn.addEventListener('mouseenter', () => (btn.style.opacity = '1'));
btn.addEventListener('mouseleave', () => (btn.style.opacity = '.95'));
btn.addEventListener('click', onDownloadClick);
document.documentElement.appendChild(btn);
const hint = document.createElement('div');
hint.id = 'jm-left-big-image-download-hint';
hint.style.cssText = `
position: fixed;
z-index: 2147483647;
top: calc(${UI_POS.top} + 48px);
right: ${UI_POS.right};
padding: 6px 10px;
border-radius: 6px;
background: rgba(0,0,0,.67);
color: #fff;
font-size: 12px;
display: none;
max-width: 42vw;
word-break: break-all;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
document.documentElement.appendChild(hint);
return btn;
}
function setBtnState(found, textExtra) {
const btn = ensureButton();
const hint = document.getElementById('jm-left-big-image-download-hint');
if (!found) {
btn.disabled = true;
btn.style.background = '#8b949e';
btn.style.cursor = 'not-allowed';
btn.textContent = '未找到左侧大图';
hint.style.display = 'none';
} else {
btn.disabled = false;
btn.style.background = '#1f6feb';
btn.style.cursor = 'pointer';
btn.textContent = '下载左侧大图';
if (textExtra) {
hint.textContent = textExtra;
hint.style.display = 'block';
} else {
hint.style.display = 'none';
}
}
}
function isVisible(el, vpH) {
const rect = el.getBoundingClientRect();
if (rect.width < 20 || rect.height < 20) return false;
if (rect.bottom <= 0 || rect.top >= vpH) return false;
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden' || +cs.opacity === 0) return false;
return true;
}
function absUrl(u) {
try {
return new URL(u, location.href).href;
} catch (e) {
return u;
}
}
function extractUrlsFromBackgroundImage(bg) {
const urls = [];
const regex = /url\((?:\"|')?([^\"')]+)(?:\"|')?\)/g;
let m;
while ((m = regex.exec(bg))) {
urls.push(m[1]);
}
return urls;
}
function getUrlFromEl(el) {
if (el.tagName === 'IMG') {
const u = el.currentSrc || el.src || '';
return u ? absUrl(u) : '';
}
const cs = getComputedStyle(el);
// 背景图、content中的 url(...)
const bgs = (cs.backgroundImage || '') + ',' + (cs.content || '');
const urls = extractUrlsFromBackgroundImage(bgs).filter(Boolean);
return urls.length ? absUrl(urls[0]) : '';
}
function pickBestLeftBigImage() {
const vpW = window.innerWidth || document.documentElement.clientWidth;
const vpH = window.innerHeight || document.documentElement.clientHeight;
const leftThreshold = vpW * 0.5;
const minArea = Math.max(MIN_ABS_AREA, vpW * vpH * MIN_VIEWPORT_AREA_RATIO);
const elems = new Set();
// img
document.querySelectorAll('img, picture img').forEach(e => elems.add(e));
// 有背景图的元素(需要遍历并看计算样式)
const all = document.querySelectorAll('*');
for (let i = 0; i < all.length; i++) {
const el = all[i];
const cs = getComputedStyle(el);
if ((cs.backgroundImage && cs.backgroundImage.includes('url(')) ||
(cs.content && cs.content.includes('url('))) {
elems.add(el);
}
}
const candidates = [];
elems.forEach(el => {
if (!isVisible(el, vpH)) return;
const rect = el.getBoundingClientRect();
if (rect.left > leftThreshold) return; // 限定在视窗左半侧
const area = rect.width * rect.height;
if (area < minArea) return;
const url = getUrlFromEl(el);
if (!url || url.startsWith('data:')) return;
const isWebp = /\.webp(\?|#|$)/i.test(url) || /[?&]format=\.?webp/i.test(url);
candidates.push({ url, area, left: rect.left, isWebp, w: rect.width, h: rect.height, tag: el.tagName });
});
// 优先:webp > 面积大 > 越靠左
candidates.sort((a, b) => {
if (a.isWebp !== b.isWebp) return a.isWebp ? -1 : 1;
if (b.area !== a.area) return b.area - a.area;
if (a.left !== b.left) return a.left - b.left;
return 0;
});
return candidates[0] || null;
}
function scheduleScan() {
if (scanTimer) clearTimeout(scanTimer);
scanTimer = setTimeout(scanAndUpdate, SCAN_DEBOUNCE_MS);
}
function scanAndUpdate() {
scanTimer = null;
const best = pickBestLeftBigImage();
if (!best) {
lastBest = null;
setBtnState(false);
return;
}
const changed = !lastBest || lastBest.url !== best.url;
lastBest = best;
const info = `${best.tag} ${Math.round(best.w)}×${Math.round(best.h)} ${best.isWebp ? 'webp' : ''}`.trim();
setBtnState(true, `${info} | ${best.url}`);
if (changed) log('found image:', best);
}
function filenameFromUrl(u) {
try {
const url = new URL(u);
let base = decodeURIComponent(url.pathname.split('/').pop() || 'left-image').replace(/[?#].*$/, '');
// 若无扩展名,按 query 中 format 或默认 webp
if (!/\.(webp|jpg|jpeg|png|gif|bmp|svg|avif|heic)$/i.test(base)) {
if (/[?&]format=\.?webp/i.test(u)) {
base += '.webp';
} else {
base += '.webp';
}
}
// 简单清洗
base = base.replace(/[^\w.\-~]+/g, '_');
return base;
} catch (e) {
return `left-image-${Date.now()}.webp`;
}
}
function gmDownload(url, name) {
return new Promise((resolve, reject) => {
if (typeof GM_download !== 'function') {
return reject(new Error('GM_download not available'));
}
try {
GM_download({
url,
name,
onload: () => resolve(),
onerror: (err) => reject(err && err.error || err || new Error('GM_download error'))
});
} catch (e) {
reject(e);
}
});
}
async function fallbackDownload(url, name) {
// 尝试 fetch -> blob(可能受 CORS 限制)
try {
const resp = await fetch(url, { mode: 'cors', credentials: 'omit' });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const blob = await resp.blob();
const objUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objUrl;
a.download = name;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(objUrl), 15000);
return;
} catch (e) {
// 最后退:直接在新标签打开(用户可另存为)
window.open(url, '_blank', 'noopener');
}
}
async function onDownloadClick() {
if (!lastBest || !lastBest.url) return;
const url = lastBest.url;
const name = filenameFromUrl(url);
const btn = ensureButton();
const prevText = btn.textContent;
btn.textContent = '下载中...';
btn.disabled = true;
btn.style.background = '#8b949e';
try {
await gmDownload(url, name);
} catch (e) {
log('GM_download failed, fallback:', e);
await fallbackDownload(url, name);
} finally {
btn.textContent = prevText;
btn.disabled = false;
btn.style.background = '#1f6feb';
}
}
function initObservers() {
const mo = new MutationObserver(() => scheduleScan());
mo.observe(document.documentElement, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['src', 'style', 'class']
});
window.addEventListener('resize', scheduleScan, { passive: true });
window.addEventListener('scroll', scheduleScan, { passive: true });
}
// 启动
ensureButton();
initObservers();
// 初次扫描(多次确保)
scanAndUpdate();
setTimeout(scanAndUpdate, 800);
setTimeout(scanAndUpdate, 1800);
})();