iStripper downloader

Replace small images by their full size and add a download button to download zip of all images + cover

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         iStripper downloader
// @description  Replace small images by their full size and add a download button to download zip of all images + cover
// @version      1.0.0
// @author       BreatFR
// @namespace    http://usercssjs.breat.fr/i/istripper
// @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() {
    const images = document.querySelectorAll('img.illustration');
    images.forEach(img => {
      if (!img.src.includes("_card_full")) img.src = img.src.replace(".jpg", "_card_full.jpg");
    });

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

    const divsWithOverlay = document.querySelectorAll('div[data-overlay]');
    divsWithOverlay.forEach(div => { div.removeAttribute('data-overlay'); });

    const overlayImages = document.querySelectorAll('img.overlay');
    overlayImages.forEach(img => { img.parentNode && img.parentNode.removeChild(img); });

    const icardDivs = document.querySelectorAll('div.img.icard');
    icardDivs.forEach(div => {
      const backgroundImageURL = div.style.backgroundImage;
      if (backgroundImageURL && !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';
      }
    });

    const icardImg = document.querySelectorAll('div.img.icard > img');
    icardImg.forEach(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";
        }
      }
    });

    const icardOverlayDivs = document.querySelectorAll('div.icard-overlay');
    icardOverlayDivs.forEach(div => {
      div.style.pointerEvents = 'none';
      div.style.position = 'absolute';
      div.style.top = '0';
      div.style.left = '0';
      div.removeAttribute('onmouseover');
    });
  }

  updateImagesAndOverlays();
  setInterval(updateImagesAndOverlays, 500);

  // ===== Helpers UI =====
  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');
    if (!icon) return;
    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 resetDownloadButton(btn) {
    btn.disabled = false;
    updateIconWithAnimation(btn, '📦', 'Download pack + cover', null);
  }

  function sanitizeZipName(name) {
    return (name || "istripper_pack").trim().replace(/[\\/:*?"<>|]/g, "_");
  }

  // ===== Binary download via GM (avoids CORS issues) =====
  function gmGetArrayBuffer(url) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url,
        responseType: "arraybuffer",
        onload: (res) => {
          if (res.status >= 200 && res.status < 300 && res.response) resolve(res.response);
          else reject(new Error(`HTTP ${res.status} for ${url}`));
        },
        onerror: () => reject(new Error(`Network error for ${url}`)),
      });
    });
  }

  async function gmGetUint8(url) {
    const ab = await gmGetArrayBuffer(url);
    return new Uint8Array(ab);
  }

  // ===== ZIP logic: unzip -> flatten photos/ -> add cover -> rezip =====
  function stripPhotosFolder(entries) {
    const out = {};
    for (const [path, data] of Object.entries(entries)) {
      if (!data || !data.length) continue;
      out[path.replace(/^photos\//i, "")] = data;
    }
    return out;
  }

  function ensureUniqueName(map, desired) {
    if (!map[desired]) return desired;
    const dot = desired.lastIndexOf(".");
    const base = dot >= 0 ? desired.slice(0, dot) : desired;
    const ext  = dot >= 0 ? desired.slice(dot) : "";
    let i = 2;
    while (map[`${base}_${i}${ext}`]) i++;
    return `${base}_${i}${ext}`;
  }

  function getOfficialZipUrl() {
    const a = document.querySelector('a > .bt-download')?.closest('a');
    if (a?.href && /\/fileaccess\/zip\/[a-z]\d+(\b|\/|$)/i.test(a.href)) return a.href;

    for (const link of document.querySelectorAll('a[href*="/fileaccess/zip/"]')) {
      if (/\/fileaccess\/zip\/[a-z]\d+(\b|\/|$)/i.test(link.href)) return link.href;
    }
    return null;
  }

  async function rebuildZipFlatWithCover({ zipUrl, coverUrl, zipNameBase, btn }) {
    updateIconWithAnimation(btn, '🌀', 'Downloading official ZIP...', 'spin');
    const zipUint8 = await gmGetUint8(zipUrl);

    updateIconWithAnimation(btn, '📦', 'Unzipping...', 'pulse');
    const entries = await new Promise((resolve, reject) => {
      fflate.unzip(zipUint8, (err, data) => (err ? reject(err) : resolve(data)));
    });

    const flat = stripPhotosFolder(entries);

    updateIconWithAnimation(btn, '🖼️', 'Downloading cover...', 'pulse');
    const coverUint8 = await gmGetUint8(coverUrl);
    const coverName = ensureUniqueName(flat, 'cover.jpg');
    flat[coverName] = coverUint8;

    updateIconWithAnimation(btn, '📦', `Creating ZIP (${Object.keys(flat).length} files)...`, null);
    const newZip = await new Promise((resolve, reject) => {
      fflate.zip(flat, { level: 0 }, (err, data) => (err ? reject(err) : resolve(data)));
    });

    const zipName = sanitizeZipName(zipNameBase) + ".zip";
    const blobUrl = URL.createObjectURL(new Blob([newZip], { type: "application/zip" }));

    updateIconWithAnimation(btn, '📥', 'Saving ZIP...', 'pulse');
    GM_download({
      url: blobUrl,
      name: zipName,
      saveAs: true,
      onload: () => setTimeout(() => URL.revokeObjectURL(blobUrl), 30_000),
      onerror: (err) => {
        URL.revokeObjectURL(blobUrl);
        console.error('[iStripper Collector] ❌ GM_download failed:', err);
        alert('ZIP download failed.');
      }
    });
  }

  // ===== Download button injection =====
  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;

    if (document.querySelector('.istripper-download-btn')) return;

    console.log('[iStripper Collector] Injecting download button');
    const btn = document.createElement('button');
    btn.className = 'istripper-download-btn';
    btn.innerHTML = `
      <div class="istripper-btn-icon">📦</div>
      <div class="istripper-btn-label">Download pack + cover</div>
    `;

    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);

      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.addEventListener('click', async () => {
      btn.disabled = true;

      try {
        const zipUrl = getOfficialZipUrl();
        if (!zipUrl) throw new Error('Official ZIP link not found.');

        const coverImg = document.querySelector('img.illustration');
        if (!coverImg?.src) throw new Error('Cover image not found.');

        const coverUrl = coverImg.src.includes('_card_full')
          ? coverImg.src.replace(/\.jpg(\?.*)?$/i, '.jpg$1')
          : coverImg.src.replace(/\.jpg(\?.*)?$/i, '_card_full.jpg$1');

        const h2 = document.querySelector('h2.mdlnav');
        const zipNameBase = h2 ? h2.textContent : 'istripper_pack';

        await rebuildZipFlatWithCover({ zipUrl, coverUrl, zipNameBase, btn });

        updateIconWithAnimation(btn, '✅', 'ZIP ready', null);
      } catch (e) {
        console.error('[iStripper Collector] ❌ Failed:', e);
        alert(`Failed: ${e?.message || e}`);

        // (plus besoin de remettre "📦 Download..." ici, le finally reset tout)
        updateIconWithAnimation(btn, '❌', 'Failed', null);
      } finally {
        // Laisse le user voir "ZIP ready" (ou "Failed") 2s, puis reset état initial
        setTimeout(() => resetDownloadButton(btn), 2000);
      }
    });
  }

  addDownloadButton();
})();