您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在即梦AI资产页添加“下载左侧大图”按钮,一键下载左侧大图(优先WebP)
// ==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); })();