Sleazy Fork is available in English.

Coomer Image Download Button

Adds a small download icon to images on Coomer post pages.

// ==UserScript==
// @name         Coomer Image Download Button
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Adds a small download icon to images on Coomer post pages.
// @match        https://coomer.st/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_download
// @connect      *
// @run-at       document-end
// @author       nereids
// @icon         https://icons.duckduckgo.com/ip3/coomer.st.ico
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  let imageCounter = 0;
  let lastUrl = location.href;

  // --- Styles ---
  GM_addStyle(`
    .coomer-dl-btn {
      position: absolute;
      bottom: 6px;
      left: 6px;
      width: 30px;
      height: 30px;
      border-radius: 50%;
      background: rgba(0,0,0,0.5);
      display: inline-flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      z-index: 2147483647;
      border: none;
      padding: 0;
    }
    .coomer-dl-btn svg { width:20px; height:20px; fill: white; pointer-events: none; }
    .coomer-dl-btn.downloading { opacity: 0.7; transform: scale(0.98); }
  `);

  // SVG download icon
  const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e3e3e3"><path d="M434.5-396.5v-329q0-19 13.25-32.25T480.5-771q19 0 32.25 13.25T526-725.5V-395l119-119q13.5-13.5 32-13.5t32 14q13.5 13.5 13.5 32t-13.5 32L512.5-253q-14 14-33.5 14t-33-14L249.5-449.5Q236-463 235.75-481.25t13.75-32.25q14-13.5 32.75-14t32.75 13l119.5 118Z"/></svg>`;

  function getUserAndPost() {
    const userMatch = location.pathname.match(/\/user\/([^/]+)/);
    const postMatch = location.pathname.match(/\/post\/(\d+)/);
    return {
      username: userMatch ? userMatch[1] : "",
      postId: postMatch ? postMatch[1] : ""
    };
  }

  function findOriginal(img) {
    const anc = img.closest('a');
    if (anc && anc.href && /\/data\//.test(anc.href)) {
      return anc.href.split('?')[0];
    }
    if (img.srcset) {
      const candidates = img.srcset.split(',').map(s => s.trim().split(/\s+/)[0]);
      for (const c of candidates) {
        if (/\/data\//.test(c)) return c.split('?')[0];
      }
    }
    const attrs = ['data-src', 'data-full', 'data-original', 'data-img', 'data-lazy-src'];
    for (const a of attrs) {
      const v = img.getAttribute(a);
      if (v && /\/data\//.test(v)) return v.split('?')[0];
    }
    if (!img.src) return null;
    try {
      const u = new URL(img.src, location.href);
      u.pathname = u.pathname.replace('/thumbnail', '');
      if (u.hostname === 'img.coomer.st') u.hostname = 'n4.coomer.st';
      u.search = '';
      return u.toString();
    } catch (e) {
      return img.src.split('?')[0];
    }
  }

  function getExtension(url) {
    const m = url.match(/\.([a-zA-Z0-9]+)(?:\?|$)/);
    return m ? "." + m[1] : ".jpg";
  }

  function buildFilename(url, username, postId) {
    imageCounter += 1;
    return `${username}-${postId}_${imageCounter}${getExtension(url)}`;
  }

  function downloadBinary(url, filename, btn) {
    if (btn) btn.classList.add('downloading');
    GM_xmlhttpRequest({
      method: 'GET',
      url,
      responseType: 'arraybuffer',
      onload(res) {
        if (btn) btn.classList.remove('downloading');
        if (res.status >= 200 && res.status < 300 && res.response) {
          let ct = 'application/octet-stream';
          if (res.responseHeaders) {
            const m = res.responseHeaders.match(/content-type:\s*([^\r\n;]+)/i);
            if (m) ct = m[1].trim();
          }
          const blob = new Blob([res.response], { type: ct });
          const blobUrl = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = blobUrl;
          a.download = filename;
          document.body.appendChild(a);
          a.click();
          a.remove();
          setTimeout(() => URL.revokeObjectURL(blobUrl), 5000);
        } else {
          console.error('Binary download failed, status:', res.status);
          fallbackDownload(url, filename);
        }
      },
      onerror(err) {
        if (btn) btn.classList.remove('downloading');
        console.error('GM_xmlhttpRequest error', err);
        fallbackDownload(url, filename);
      },
      ontimeout() {
        if (btn) btn.classList.remove('downloading');
        console.error('GM_xmlhttpRequest timeout');
        fallbackDownload(url, filename);
      }
    });
  }

  function fallbackDownload(url, filename) {
    try {
      if (typeof GM_download === 'function') {
        try {
          GM_download({ url, name: filename });
          return;
        } catch (e) {
          GM_download(url, filename);
          return;
        }
      }
    } catch (e) {
      console.warn('GM_download fallback failed', e);
    }
    window.open(url, '_blank');
  }

  function addButtonsOnce() {
    const { username, postId } = getUserAndPost();
    if (!username || !postId) return; // only on post pages

    const images = Array.from(document.querySelectorAll('img'));
    images.forEach(img => {
      if (img.dataset.cdlAdded) return;
      if (!img.src || (!img.src.includes('/thumbnail/') && !img.src.includes('/data/'))) {
        img.dataset.cdlAdded = '1';
        return;
      }

      const orig = findOriginal(img);
      if (!orig) {
        img.dataset.cdlAdded = '1';
        return;
      }

      const parent = img.parentElement || img;
      const cs = getComputedStyle(parent);
      if (cs.position === 'static') {
        parent.style.position = 'relative';
        parent.dataset._cdl_posset = '1';
      }

      const btn = document.createElement('button');
      btn.className = 'coomer-dl-btn';
      btn.type = 'button';
      btn.title = 'Download full-size image';
      btn.innerHTML = ICON_SVG;
      btn.addEventListener('click', function (e) {
        e.stopPropagation();
        e.preventDefault();
        const filename = buildFilename(orig, username, postId);
        downloadBinary(orig, filename, btn);
      }, { capture: true });

      parent.appendChild(btn);
      img.dataset.cdlAdded = '1';
    });
  }

  function runForPost() {
    const { username, postId } = getUserAndPost();
    if (username && postId) {
      imageCounter = 0; // reset per post
      addButtonsOnce();
    }
  }

  // Watch for DOM changes
  const mo = new MutationObserver(addButtonsOnce);
  mo.observe(document.body, { childList: true, subtree: true });

  // Watch for URL changes (SPA navigation)
  function checkUrlChange() {
    if (location.href !== lastUrl) {
      lastUrl = location.href;
      runForPost();
    }
  }
  setInterval(checkUrlChange, 500);

  // Initial run
  runForPost();

})();