Random Image Downloader (Alt+Shift+D)

Download a random image from the current page. Click the floating button or press Alt+Shift+D.

// ==UserScript==
// @name         Random Image Downloader (Alt+Shift+D)
// @namespace    https://greasyfork.org/en/users/your-name
// @version      1.0.0
// @description  Download a random image from the current page. Click the floating button or press Alt+Shift+D.
// @author       Your Name
// @license      MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @connect      *
// ==/UserScript==

(function () {
  'use strict';

  // ---- helpers -------------------------------------------------------------
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  function pickImages() {
    // Grab all <img> plus common meta images (og:image) as fallbacks
    const imgTags = Array.from(document.querySelectorAll('img[src]'))
      .map(img => img.src)
      .filter(Boolean);

    const og = document.querySelector('meta[property="og:image"], meta[name="og:image"]');
    const ogUrl = og?.content?.trim();

    const candidates = [...new Set([...imgTags, ...(ogUrl ? [ogUrl] : [])])];

    // Filter out obvious sprites/very tiny images
    const filtered = candidates.filter((url) => {
      const u = url.toLowerCase();
      if (u.startsWith('data:')) return false;
      if (u.endsWith('.svg')) return false; // optional: skip SVGs
      // Skip tracking pixels and favicons
      if (u.includes('pixel') || u.includes('tracker') || u.includes('favicon')) return false;
      return true;
    });

    return filtered.length ? filtered : candidates;
  }

  function filenameFromUrl(url) {
    try {
      const u = new URL(url, location.href);
      const base = u.pathname.split('/').filter(Boolean).pop() || 'image';
      const clean = base.split('?')[0].split('#')[0];
      const ts = new Date().toISOString().replace(/[:.]/g, '-');
      return clean || `image-${ts}.jpg`;
    } catch {
      return `image-${Date.now()}.jpg`;
    }
  }

  function downloadViaBlob(url) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        responseType: 'blob',
        onload: (res) => {
          if (!res.response) return reject(new Error('Empty response'));
          const blob = res.response;
          const blobUrl = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = blobUrl;
          a.download = filenameFromUrl(url);
          document.body.appendChild(a);
          a.click();
          a.remove();
          URL.revokeObjectURL(blobUrl);
          resolve();
        },
        onerror: (e) => reject(e instanceof Error ? e : new Error('Request failed')),
      });
    });
  }

  function createToast() {
    const el = document.createElement('div');
    el.style.position = 'fixed';
    el.style.zIndex = '2147483647';
    el.style.right = '16px';
    el.style.bottom = '16px';
    el.style.padding = '10px 14px';
    el.style.borderRadius = '12px';
    el.style.background = 'rgba(0,0,0,0.8)';
    el.style.color = '#fff';
    el.style.font = '14px/1.3 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif';
    el.style.boxShadow = '0 6px 24px rgba(0,0,0,0.2)';
    el.style.pointerEvents = 'none';
    el.style.opacity = '0';
    el.style.transition = 'opacity .18s ease';
    document.documentElement.appendChild(el);
    return {
      show(msg) {
        el.textContent = msg;
        el.style.opacity = '1';
        clearTimeout(el._t);
        el._t = setTimeout(() => (el.style.opacity = '0'), 2200);
      }
    };
  }

  function createButton(onClick) {
    const btn = document.createElement('button');
    btn.textContent = '↓ Random Image';
    Object.assign(btn.style, {
      position: 'fixed',
      right: '16px',
      bottom: '64px',
      zIndex: 2147483647,
      padding: '10px 12px',
      borderRadius: '999px',
      border: 'none',
      font: '13px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
      boxShadow: '0 6px 24px rgba(0,0,0,0.2)',
      cursor: 'pointer',
      background: '#111',
      color: '#fff',
      opacity: '0.9'
    });
    btn.addEventListener('mouseenter', () => (btn.style.opacity = '1'));
    btn.addEventListener('mouseleave', () => (btn.style.opacity = '0.9'));
    btn.addEventListener('click', onClick);
    document.documentElement.appendChild(btn);
    return btn;
  }

  // ---- main ---------------------------------------------------------------
  const toast = createToast();

  async function runDownload() {
    const list = pickImages();
    if (!list.length) {
      toast.show('No images found on this page.');
      return;
    }
    const pick = list[Math.floor(Math.random() * list.length)];

    toast.show('Downloading image...');
    try {
      await downloadViaBlob(pick);
      toast.show('Image downloaded.');
    } catch (e) {
      // Fallback: try direct navigation if blob download fails
      try {
        const a = document.createElement('a');
        a.href = pick;
        a.target = '_blank';
        a.rel = 'noopener';
        document.body.appendChild(a);
        a.click();
        a.remove();
        toast.show('Opened image in a new tab (save it manually).');
      } catch {
        toast.show('Download failed.');
      }
    }
  }

  // Add UI button
  createButton(runDownload);

  // Add keyboard shortcut: Alt+Shift+D
  window.addEventListener('keydown', (e) => {
    if (e.altKey && e.shiftKey && (e.key === 'D' || e.key === 'd')) {
      e.preventDefault();
      runDownload();
    }
  });

  // Optional: delay a bit so images can load on heavy pages
  (async () => {
    await sleep(800);
    // no-op; ensures DOM is mostly ready
  })();
})();