iStripper features

Replace small images by their full size and add a download button

// ==UserScript==
// @name         iStripper features
// @description  Replace small images by their full size and add a download button
// @version      1.0.0
// @author       BreatFR
// @namespace    http://gitlab.com/breatfr
// @match        *://*.istripper.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js
// @copyright    2025, BreatFR (https://breat.fr)
// @icon         https://www.istripper.com/favicons/istripper/apple-icon-120x120.png
// @license      AGPL-3.0-or-later; https://www.gnu.org/licenses/agpl-3.0.txt
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        a.box:hover div.img.icard {
            background-position: initial;
        }
        [src*="https://www.istripper.com/free/sets"] {
            transition: transform .2s ease-in-out;
        }
        a.box:hover [src*="https://www.istripper.com/free/sets"] {
            transform: scale(1.1);
            transition: transform .2s ease-in-out;
        }

        .istripper-download-btn {
            align-items: center;
            background-color: rgba(24, 24, 24, .2);
            border: none;
            border-radius: .5em;
            color: #fff;
            cursor: pointer;
            display: flex;
            flex-direction: column;
            font-family: poppins, cursive;
            font-size: 1.15rem;
            gap: 1em;
            justify-content: center;
            line-height: 1.5;
            padding: .5em 1em;
            position: absolute;
            transition: background-color .3s ease, box-shadow .3s ease;
            z-index: 9999;
        }
        .istripper-download-btn:hover {
            background-color: rgba(255, 80, 80, .85);
            box-shadow: 0 0 2em rgba(255, 80, 80, .85);
        }
        @keyframes spinLoop {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
        .istripper-btn-icon.spin {
            animation: spinLoop 1s linear infinite;
        }
        @keyframes pulseLoop {
            0%   { transform: scale(1); opacity: 1; }
            50%  { transform: scale(1.1); opacity: 0.7; }
            100% { transform: scale(1); opacity: 1; }
        }
        .istripper-btn-icon.pulse {
            animation: pulseLoop 1.8s ease-in-out infinite;
        }
        .istripper-btn-icon {
            font-size: 3em;
            line-height: 1em;
        }
        #top {
            aspect-ratio: 1 / 1;
            background: transparent;
            border: none;
            bottom: 1em;
            cursor: pointer;
            font-size: 1.2em;
            left: 1em;
            position: fixed;
            z-index: 9999;
        }
    `;
    document.head.appendChild(style);

    function updateImagesAndOverlays() {
        let images = document.querySelectorAll('img.illustration');

        images.forEach(function(img) {
            if (!img.src.includes("_card_full")) {
                img.src = img.src.replace(".jpg", "_card_full.jpg");
            }
        });

        let divCollection = document.querySelectorAll('.collection-label');
        divCollection.forEach(function(div) {
            div.style.pointerEvents = 'none';
        });

        let divsWithOverlay = document.querySelectorAll('div[data-overlay]');

        divsWithOverlay.forEach(function(div) {
            div.removeAttribute('data-overlay');
        });

        let overlayImages = document.querySelectorAll('img.overlay');

        overlayImages.forEach(function(img) {
            img.parentNode.removeChild(img);
        });

        let icardDivs = document.querySelectorAll('div.img.icard');

        icardDivs.forEach(function(div) {
            let backgroundImageURL = div.style.backgroundImage;
            if (backgroundImageURL) {
                if (!backgroundImageURL.includes("_card_full")) {
                    div.style.alignItems = 'center';
                    div.style.backgroundPosition = 'center center';
                    div.style.backgroundSize = '0';
                    div.style.display = 'flex';
                    div.style.justifyContent = 'center';
                }
            }
        });

        let icardImg = document.querySelectorAll('div.img.icard > img');
        icardImg.forEach(function(img) {
            if (!img.src.includes("_card_full") && img.classList.contains("hidden")) {
                if (img.src.endsWith(".jpg")) {
                    img.src = img.src.replace(".jpg", "_card_full.jpg");
                    img.removeAttribute('class');
                    img.style.height = "242px";
                    img.style.opacity = '1';
                    img.style.pointerEvents = 'auto';
                    img.style.width = "auto";
                } else {
                    console.log("Image URL does not end with '.jpg':", img.src);
                }
            }
        });

        let icardOverlayDivs = document.querySelectorAll('div.icard-overlay');

        icardOverlayDivs.forEach(function(div) {
            div.style.pointerEvents = 'none';
            div.style.position = 'absolute';
            div.style.top = '0';
            div.style.left = '0';
            div.removeAttribute('onmouseover');
        });
    }

    updateImagesAndOverlays();

    setInterval(updateImagesAndOverlays, 500);

    // Download button
    async function downloadImage(url) {
      console.log(`[iStripper Collector] Fetching image: ${url}`);
      const response = await fetch(url, { mode: 'cors' });
      if (!response.ok) throw new Error(`Fetch failed for ${url}`);
      const blob = await response.blob();
      return { blob };
    }

    function extractFilename(url) {
      const match = url.match(/\/([^\/]+?\.(jpg|jpeg|png|webp|gif))(?=\/|$)/i);
      return match ? match[1] : 'image.jpg';
    }

    function blobToUint8Array(blob) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(new Uint8Array(reader.result));
        reader.onerror = reject;
        reader.readAsArrayBuffer(blob);
      });
    }

    function setButtonContent(btn, icon, label) {
      let iconEl = btn.querySelector('.istripper-btn-icon');
      let labelEl = btn.querySelector('.istripper-btn-label');

      if (!iconEl) {
        iconEl = document.createElement('div');
        iconEl.className = 'istripper-btn-icon';
        btn.appendChild(iconEl);
      }
      if (!labelEl) {
        labelEl = document.createElement('div');
        labelEl.className = 'istripper-btn-label';
        btn.appendChild(labelEl);
      }

      iconEl.textContent = icon;
      labelEl.textContent = label;
    }

    function setIconAnimation(btn, type) {
      const icon = btn.querySelector('.istripper-btn-icon');
      icon.classList.remove('spin', 'pulse');
      void icon.offsetWidth;
      if (type) icon.classList.add(type);
    }

    function updateIconWithAnimation(btn, icon, label, animationClass) {
      setButtonContent(btn, icon, label);
      requestAnimationFrame(() => setIconAnimation(btn, animationClass));
    }

    function addDownloadButton() {
      const urlMatch = location.href.match(/^https:\/\/www\.istripper\.com\/[^\/]+\/models\/[^\/]+\/[^\/]+$/);
      const isPackPage = urlMatch && document.querySelector('img.illustration') && document.querySelectorAll('a.picture').length > 0;
      const hasDownloadLink = !!document.querySelector('a > .bt-download');
      if (!isPackPage || !hasDownloadLink) return;

      console.log('[iStripper Collector] Injecting download button');
      const btn = document.createElement('button');
      const blackBox = document.querySelector('div.cta.showDetail');
      const wrapper = document.querySelector('div[style="background: #f6f6f6;"]');

      if (blackBox && wrapper) {
        wrapper.style.position = 'relative';
        wrapper.appendChild(btn); // injecte d'abord pour que offsetWidth soit dispo

        requestAnimationFrame(() => {
          const boxRect = blackBox.getBoundingClientRect();
          const wrapperRect = wrapper.getBoundingClientRect();
          const btnRect = btn.getBoundingClientRect();

          const top = boxRect.top - wrapperRect.top - btnRect.height - 32;
          const left = boxRect.left - wrapperRect.left + (boxRect.width / 2) - (btnRect.width / 2);

          btn.style.top = `${top}px`;
          btn.style.left = `${left}px`;
        });
      } else {
        console.warn('[iStripper Collector] Positioning fallback');
        document.body.appendChild(btn);
      }

      btn.className = 'istripper-download-btn';
      btn.innerHTML = `
        <div class="istripper-btn-icon">📦</div>
        <div class="istripper-btn-label">Download images pack + cover</div>
      `;

      const targetDiv = document.querySelector('div[style="background: #f6f6f6;"]');
      if (targetDiv) {
        targetDiv.appendChild(btn);
      } else {
        console.warn('[iStripper Collector] Target div not found, appending to body as fallback');
        document.body.appendChild(btn);
      }

      btn.addEventListener('click', async () => {
        btn.disabled = true;
        updateIconWithAnimation(btn, '🌀', 'Collecting images...', 'spin');

        const coverImg = document.querySelector('img.illustration');
        const pictureLinks = Array.from(document.querySelectorAll('a.picture'))
          .map(a => a.href)
          .filter(href => href.includes('.jpg'));

        const allLinks = [];
        if (coverImg && coverImg.src) allLinks.push({ url: coverImg.src, name: 'cover' });
        pictureLinks.forEach(url => allLinks.push({ url, name: null }));

        console.log(`[iStripper Collector] Found ${allLinks.length} images`);

        const files = {};
        let count = 0;

        for (const { url, name } of allLinks) {
          try {
            const { blob } = await downloadImage(url);
            if (!blob || blob.size === 0) {
              console.warn(`[iStripper Collector] Skipped empty blob: ${url}`);
              continue;
            }

            const uint8 = await blobToUint8Array(blob);
            if (!(uint8 instanceof Uint8Array) || uint8.length === 0) {
              console.warn(`[iStripper Collector] Skipped invalid Uint8Array for ${url}`);
              continue;
            }

            const originalName = extractFilename(url);
            const finalName = name === 'cover' ? `cover.${originalName.split('.').pop()}` : originalName;

            console.log(`[iStripper Collector] Preparing ZIP entry: ${finalName} (${uint8.length} bytes)`);
            files[finalName] = uint8;
            count++;
            updateIconWithAnimation(btn, '📥', `Downloading ${count}/${allLinks.length}`, 'pulse');
          } catch (e) {
            console.warn(`[iStripper Collector] Failed to download ${url}`, e);
          }
        }

        if (count === 0) {
          alert('No images downloaded.');
          btn.disabled = false;
          updateIconWithAnimation(btn, '📦', 'Download pack + cover', null);
          return;
        }

        updateIconWithAnimation(btn, '📦', `Creating ZIP (${count} images)...`, null);

        try {
          const zipped = await new Promise((resolve, reject) => {
            fflate.zip(files, {}, (err, data) => {
              if (err) {
                console.error('[ZIP async error]', err);
                reject(err);
              } else {
                resolve(data);
              }
            });
          });

          const blob = new Blob([zipped], { type: 'application/zip' });
          const zipName = document.title.replace(/[\\/:*?"<>|]/g, '_') || 'istripper_pack';

          GM_download({
            url: URL.createObjectURL(blob),
            name: `${zipName}.zip`,
            saveAs: true,
            onerror: err => {
              console.error('[iStripper Collector] ❌ GM_download failed:', err);
              alert('ZIP download failed.');
            }
          });
        } catch (e) {
          console.error('[iStripper Collector] ❌ ZIP creation failed:', e);
          alert(`ZIP creation failed: ${e?.message || e}`);
        }

        updateIconWithAnimation(btn, '✅', `${count} images downloaded`, null);
        setTimeout(() => {
          updateIconWithAnimation(btn, '📦', 'Download pack + cover', null);
          btn.disabled = false;
        }, 3000);
      });
    }

    addDownloadButton();

    // Back to top
    const btn = document.createElement('button');
        btn.id = 'top';
        btn.setAttribute('aria-label', 'Scroll to top');
        btn.setAttribute('title', 'Scroll to top');
        setButtonContent(btn, '🔝', '')

    document.body.appendChild(btn);

    const mybutton = document.getElementById("top");

    window.onscroll = function () {
        scrollFunction();
    };

    function scrollFunction() {
        if (
            document.body.scrollTop > 20 ||
            document.documentElement.scrollTop > 20
        ) {
            mybutton.style.display = "block";
        } else {
            mybutton.style.display = "none";
        }
    }

    mybutton.addEventListener("click", backToTop);

    function backToTop() {
        window.scrollTo({ top: 0, behavior: 'smooth' });
    }
})();