Easy Image Downloader (hover button)

Adds a small download button on images; click to save the original image (uses GM_download when available).

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Easy Image Downloader (hover button)
// @namespace    https://greasyfork.org/en/users/your-name
// @version      1.0.0
// @description  Adds a small download button on images; click to save the original image (uses GM_download when available).
// @author       you
// @license      MIT
// @match        *://*/*
// @exclude      *://greasyfork.org/*
// @run-at       document-idle
// @grant        GM_download
// ==/UserScript==

(function () {
  "use strict";

  // ========== Config ==========
  const MIN_W = 120;                 // only show button on images >= MIN_W x MIN_H
  const MIN_H = 120;
  const BUTTON_TEXT = "↓";
  const BUTTON_CLASS = "eid-btn";
  const BUTTON_STYLES = `
    .${BUTTON_CLASS} {
      position: absolute;
      right: 6px;
      bottom: 6px;
      z-index: 2147483647;
      padding: 4px 8px;
      font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
      background: rgba(0,0,0,.75);
      color: #fff;
      border: 0;
      border-radius: 6px;
      cursor: pointer;
      opacity: 0;
      transition: opacity .15s ease-in-out, transform .15s ease-in-out;
      transform: translateY(2px);
      user-select: none;
    }
    .eid-wrap:hover .${BUTTON_CLASS} { opacity: 1; transform: translateY(0); }
  `;

  // Inject CSS
  const style = document.createElement("style");
  style.textContent = BUTTON_STYLES;
  document.documentElement.appendChild(style);

  // Observe images added later
  const observer = new MutationObserver(() => decorateAll());
  observer.observe(document.documentElement, { childList: true, subtree: true });

  // Initial pass
  decorateAll();

  function decorateAll() {
    const imgs = document.querySelectorAll("img:not([data-eid])");
    for (const img of imgs) {
      // Skip tiny/hidden images
      const w = img.naturalWidth || img.width;
      const h = img.naturalHeight || img.height;
      if (w < MIN_W || h < MIN_H) {
        img.setAttribute("data-eid", "skip");
        continue;
      }

      // Wrap the image in a relatively positioned container (non-destructive)
      const wrap = document.createElement("span");
      wrap.className = "eid-wrap";
      wrap.style.position = "relative";
      wrap.style.display = "inline-block";

      // Some sites have display:block on images; preserve layout width/height
      wrap.style.width = img.width ? img.width + "px" : "";
      wrap.style.height = img.height ? img.height + "px" : "";

      // Insert wrapper and move the image inside
      img.parentNode && img.parentNode.insertBefore(wrap, img);
      wrap.appendChild(img);

      // Create button
      const btn = document.createElement("button");
      btn.type = "button";
      btn.className = BUTTON_CLASS;
      btn.textContent = BUTTON_TEXT;
      btn.title = "Download image";
      btn.addEventListener("click", (ev) => {
        ev.preventDefault();
        ev.stopPropagation();
        const url = resolveImageURL(img);
        const filename = suggestFilename(img, url);
        downloadImage(url, filename);
      });

      wrap.appendChild(btn);
      img.setAttribute("data-eid", "done");
    }
  }

  // Try to get the highest quality URL if the site uses srcset
  function resolveImageURL(img) {
    // Prefer currentSrc when available (handles srcset)
    if (img.currentSrc) return img.currentSrc;
    return img.src || "";
  }

  function suggestFilename(img, url) {
    try {
      const u = new URL(url, location.href);
      const pathName = u.pathname.split("/").pop() || "image";
      const clean = decodeURIComponent(pathName).split("?")[0].split("#")[0];
      const base = clean || (img.alt ? sluggify(img.alt) : "image");
      const ext = guessExtFromURL(url) || "jpg";
      return ensureExt(base, ext);
    } catch {
      const base = (img.alt ? sluggify(img.alt) : "image");
      return ensureExt(base, "jpg");
    }
  }

  function sluggify(s) {
    return s
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "")
      .slice(0, 64) || "image";
  }

  function guessExtFromURL(url) {
    const m = url.toLowerCase().match(/\.(png|jpe?g|webp|gif|bmp|svg|avif)(?:$|\?|\#)/);
    return m ? m[1].replace("jpeg", "jpg") : null;
  }

  function ensureExt(name, ext) {
    if (!name.toLowerCase().endsWith(`.${ext}`)) return `${name}.${ext}`;
    return name;
  }

  async function downloadImage(url, filename) {
    // Prefer GM_download when available (cross-origin friendly)
    if (typeof GM_download === "function") {
      try {
        GM_download({
          url,
          name: filename,
          headers: { Referer: location.href }, // helps on some hosts
          onerror: () => fallbackDownload(url, filename),
        });
        return;
      } catch {
        // fall through
      }
    }
    // Fallback
    fallbackDownload(url, filename);
  }

  async function fallbackDownload(url, filename) {
    try {
      // If same-origin or CORS allowed
      const res = await fetch(url, { credentials: "omit" });
      const blob = await res.blob();
      const a = document.createElement("a");
      const objectUrl = URL.createObjectURL(blob);
      a.href = objectUrl;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      setTimeout(() => {
        URL.revokeObjectURL(objectUrl);
        a.remove();
      }, 1000);
    } catch (e) {
      // Last resort: open in a new tab (user can save manually)
      window.open(url, "_blank", "noopener,noreferrer");
      console.warn("[Easy Image Downloader] Fallback open:", e);
    }
  }
})();