Greasy Fork

Greasy Fork is available in English.

即梦AI左侧大图下载按钮

在即梦AI资产页添加“下载左侧大图”按钮,一键下载左侧大图(优先WebP)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
})();