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 για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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